From 017731359578064997f084020c9f411e61577f1c Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Fri, 20 Sep 2024 17:34:20 +0900 Subject: [PATCH 01/19] Add number platform to LG ThinQ integration (#125711) * Add number platform to LG ThinQ integration * Add number platform to LG ThinQ integration * Update for shebangs * Modify update_status --------- Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/__init__.py | 2 +- homeassistant/components/lg_thinq/icons.json | 50 ++++ homeassistant/components/lg_thinq/number.py | 214 ++++++++++++++++++ .../components/lg_thinq/strings.json | 50 ++++ 4 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lg_thinq/number.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index 625938564a8..f11bbd749d3 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -20,7 +20,7 @@ from .coordinator import DeviceDataUpdateCoordinator, async_setup_device_coordin type ThinqConfigEntry = ConfigEntry[dict[str, DeviceDataUpdateCoordinator]] -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index d96214725c8..13b453c23ad 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -39,6 +39,56 @@ "one_touch_filter": { "default": "mdi:air-filter" } + }, + "number": { + "target_temperature": { + "default": "mdi:thermometer" + }, + "target_temperature_for_location": { + "default": "mdi:thermometer" + }, + "light_status": { + "default": "mdi:television-ambient-light" + }, + "fan_speed": { + "default": "mdi:wind-power-outline" + }, + "lamp_brightness": { + "default": "mdi:alarm-light-outline" + }, + "wind_temperature": { + "default": "mdi:thermometer" + }, + "relative_hour_to_start": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_start_for_location": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_start_wm": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_start_wm_for_location": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_stop": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_stop_for_location": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_stop_wm": { + "default": "mdi:timer-edit-outline" + }, + "relative_hour_to_stop_wm_for_location": { + "default": "mdi:timer-edit-outline" + }, + "sleep_timer_relative_hour_to_stop": { + "default": "mdi:bed-clock" + }, + "sleep_timer_relative_hour_to_stop_for_location": { + "default": "mdi:bed-clock" + } } } } diff --git a/homeassistant/components/lg_thinq/number.py b/homeassistant/components/lg_thinq/number.py new file mode 100644 index 00000000000..4e5e53def6f --- /dev/null +++ b/homeassistant/components/lg_thinq/number.py @@ -0,0 +1,214 @@ +"""Support for number entities.""" + +from __future__ import annotations + +import logging + +from thinqconnect import DeviceType +from thinqconnect.devices.const import Property as ThinQProperty +from thinqconnect.integration import ActiveMode, TimerProperty + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import PERCENTAGE, UnitOfTemperature, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ThinqConfigEntry +from .entity import ThinQEntity + +NUMBER_DESC: dict[ThinQProperty, NumberEntityDescription] = { + ThinQProperty.FAN_SPEED: NumberEntityDescription( + key=ThinQProperty.FAN_SPEED, + translation_key=ThinQProperty.FAN_SPEED, + ), + ThinQProperty.LAMP_BRIGHTNESS: NumberEntityDescription( + key=ThinQProperty.LAMP_BRIGHTNESS, + translation_key=ThinQProperty.LAMP_BRIGHTNESS, + ), + ThinQProperty.LIGHT_STATUS: NumberEntityDescription( + key=ThinQProperty.LIGHT_STATUS, + native_unit_of_measurement=PERCENTAGE, + translation_key=ThinQProperty.LIGHT_STATUS, + ), + ThinQProperty.TARGET_HUMIDITY: NumberEntityDescription( + key=ThinQProperty.TARGET_HUMIDITY, + device_class=NumberDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + ThinQProperty.TARGET_TEMPERATURE: NumberEntityDescription( + key=ThinQProperty.TARGET_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + translation_key=ThinQProperty.TARGET_TEMPERATURE, + ), + ThinQProperty.WIND_TEMPERATURE: NumberEntityDescription( + key=ThinQProperty.WIND_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + translation_key=ThinQProperty.WIND_TEMPERATURE, + ), +} +TIMER_NUMBER_DESC: dict[ThinQProperty, NumberEntityDescription] = { + ThinQProperty.RELATIVE_HOUR_TO_START: NumberEntityDescription( + key=ThinQProperty.RELATIVE_HOUR_TO_START, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=ThinQProperty.RELATIVE_HOUR_TO_START, + ), + TimerProperty.RELATIVE_HOUR_TO_START_WM: NumberEntityDescription( + key=ThinQProperty.RELATIVE_HOUR_TO_START, + native_min_value=0, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=TimerProperty.RELATIVE_HOUR_TO_START_WM, + ), + ThinQProperty.RELATIVE_HOUR_TO_STOP: NumberEntityDescription( + key=ThinQProperty.RELATIVE_HOUR_TO_STOP, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=ThinQProperty.RELATIVE_HOUR_TO_STOP, + ), + TimerProperty.RELATIVE_HOUR_TO_STOP_WM: NumberEntityDescription( + key=ThinQProperty.RELATIVE_HOUR_TO_STOP, + native_min_value=0, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=TimerProperty.RELATIVE_HOUR_TO_STOP_WM, + ), + ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP: NumberEntityDescription( + key=ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP, + ), +} +WASHER_NUMBERS: tuple[NumberEntityDescription, ...] = ( + TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_START_WM], + TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_STOP_WM], +) + +DEVICE_TYPE_NUMBER_MAP: dict[DeviceType, tuple[NumberEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: ( + TIMER_NUMBER_DESC[ThinQProperty.RELATIVE_HOUR_TO_START], + TIMER_NUMBER_DESC[ThinQProperty.RELATIVE_HOUR_TO_STOP], + TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP], + ), + DeviceType.AIR_PURIFIER_FAN: ( + NUMBER_DESC[ThinQProperty.WIND_TEMPERATURE], + TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP], + ), + DeviceType.DRYER: WASHER_NUMBERS, + DeviceType.HOOD: ( + NUMBER_DESC[ThinQProperty.LAMP_BRIGHTNESS], + NUMBER_DESC[ThinQProperty.FAN_SPEED], + ), + DeviceType.HUMIDIFIER: ( + NUMBER_DESC[ThinQProperty.TARGET_HUMIDITY], + TIMER_NUMBER_DESC[ThinQProperty.SLEEP_TIMER_RELATIVE_HOUR_TO_STOP], + ), + DeviceType.MICROWAVE_OVEN: ( + NUMBER_DESC[ThinQProperty.LAMP_BRIGHTNESS], + NUMBER_DESC[ThinQProperty.FAN_SPEED], + ), + DeviceType.OVEN: (NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],), + DeviceType.REFRIGERATOR: (NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],), + DeviceType.STYLER: (TIMER_NUMBER_DESC[TimerProperty.RELATIVE_HOUR_TO_STOP_WM],), + DeviceType.WASHCOMBO_MAIN: WASHER_NUMBERS, + DeviceType.WASHCOMBO_MINI: WASHER_NUMBERS, + DeviceType.WASHER: WASHER_NUMBERS, + DeviceType.WASHTOWER_DRYER: WASHER_NUMBERS, + DeviceType.WASHTOWER: WASHER_NUMBERS, + DeviceType.WASHTOWER_WASHER: WASHER_NUMBERS, + DeviceType.WATER_HEATER: (NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE],), + DeviceType.WINE_CELLAR: ( + NUMBER_DESC[ThinQProperty.LIGHT_STATUS], + NUMBER_DESC[ThinQProperty.TARGET_TEMPERATURE], + ), +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for number platform.""" + entities: list[ThinQNumberEntity] = [] + for coordinator in entry.runtime_data.values(): + if ( + descriptions := DEVICE_TYPE_NUMBER_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQNumberEntity(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 ThinQNumberEntity(ThinQEntity, NumberEntity): + """Represent a thinq number platform.""" + + _attr_mode = NumberMode.BOX + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + self._attr_native_value = self.data.value + + # Update unit. + if ( + unit_of_measurement := self._get_unit_of_measurement(self.data.unit) + ) is not None: + self._attr_native_unit_of_measurement = unit_of_measurement + + # Undate range. + if ( + self.entity_description.native_min_value is None + and (min_value := self.data.min) is not None + ): + self._attr_native_min_value = min_value + + if ( + self.entity_description.native_max_value is None + and (max_value := self.data.max) is not None + ): + self._attr_native_max_value = max_value + + if ( + self.entity_description.native_step is None + and (step := self.data.step) is not None + ): + self._attr_native_step = step + + _LOGGER.debug( + "[%s:%s] update status: %s -> %s, unit:%s, min:%s, max:%s, step:%s", + self.coordinator.device_name, + self.property_id, + self.data.value, + self.native_value, + self.native_unit_of_measurement, + self.native_min_value, + self.native_max_value, + self.native_step, + ) + + async def async_set_native_value(self, value: float) -> None: + """Change to new number value.""" + if self.step.is_integer(): + value = int(value) + _LOGGER.debug( + "[%s:%s] async_set_native_value: %s", + self.coordinator.device_name, + self.property_id, + value, + ) + + await self.async_call_api(self.coordinator.api.post(self.property_id, value)) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 9ec11952a9a..8b54d9461c4 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -58,6 +58,56 @@ "one_touch_filter": { "name": "Fresh air filter" } + }, + "number": { + "target_temperature": { + "name": "Temperature" + }, + "target_temperature_for_location": { + "name": "{location} temperature" + }, + "light_status": { + "name": "Light" + }, + "fan_speed": { + "name": "Fan" + }, + "lamp_brightness": { + "name": "Light" + }, + "wind_temperature": { + "name": "Wind temperature" + }, + "relative_hour_to_start": { + "name": "Schedule turn-on" + }, + "relative_hour_to_start_for_location": { + "name": "{location} schedule turn-on" + }, + "relative_hour_to_start_wm": { + "name": "Delay starts in" + }, + "relative_hour_to_start_wm_for_location": { + "name": "{location} delay starts in" + }, + "relative_hour_to_stop": { + "name": "Schedule turn-off" + }, + "relative_hour_to_stop_for_location": { + "name": "{location} schedule turn-off" + }, + "relative_hour_to_stop_wm": { + "name": "Delay ends in" + }, + "relative_hour_to_stop_wm_for_location": { + "name": "{location} delay ends in" + }, + "sleep_timer_relative_hour_to_stop": { + "name": "Sleep timer" + }, + "sleep_timer_relative_hour_to_stop_for_location": { + "name": "{location} sleep timer" + } } } } From 527cd368f036df4e9762c70205c785139f352bb2 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Fri, 20 Sep 2024 17:46:13 +0900 Subject: [PATCH 02/19] Add event platform to LG ThinQ integration (#125800) * Add event platform to LG ThinQ integration * Add error and notification translations * Update event.py * Fix translations * Fix --------- Co-authored-by: jangwon.lee Co-authored-by: Joostlek --- homeassistant/components/lg_thinq/__init__.py | 7 +- homeassistant/components/lg_thinq/event.py | 120 ++++++++++++++++++ homeassistant/components/lg_thinq/icons.json | 8 ++ .../components/lg_thinq/strings.json | 110 +++++++++++++++- 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/lg_thinq/event.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index f11bbd749d3..c39d07a1d2a 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -20,7 +20,12 @@ from .coordinator import DeviceDataUpdateCoordinator, async_setup_device_coordin type ThinqConfigEntry = ConfigEntry[dict[str, DeviceDataUpdateCoordinator]] -PLATFORMS = [Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.EVENT, + Platform.NUMBER, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lg_thinq/event.py b/homeassistant/components/lg_thinq/event.py new file mode 100644 index 00000000000..c9852f968b6 --- /dev/null +++ b/homeassistant/components/lg_thinq/event.py @@ -0,0 +1,120 @@ +"""Support for event entity.""" + +from __future__ import annotations + +import logging + +from thinqconnect import DeviceType +from thinqconnect.integration import ActiveMode, ThinQPropertyEx + +from homeassistant.components.event import EventEntity, EventEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + +NOTIFICATION_EVENT_DESC = EventEntityDescription( + key=ThinQPropertyEx.NOTIFICATION, + translation_key=ThinQPropertyEx.NOTIFICATION, +) +ERROR_EVENT_DESC = EventEntityDescription( + key=ThinQPropertyEx.ERROR, + translation_key=ThinQPropertyEx.ERROR, +) +ALL_EVENTS: tuple[EventEntityDescription, ...] = ( + ERROR_EVENT_DESC, + NOTIFICATION_EVENT_DESC, +) +DEVICE_TYPE_EVENT_MAP: dict[DeviceType, tuple[EventEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: (NOTIFICATION_EVENT_DESC,), + DeviceType.AIR_PURIFIER_FAN: (NOTIFICATION_EVENT_DESC,), + DeviceType.AIR_PURIFIER: (NOTIFICATION_EVENT_DESC,), + DeviceType.DEHUMIDIFIER: (NOTIFICATION_EVENT_DESC,), + DeviceType.DISH_WASHER: ALL_EVENTS, + DeviceType.DRYER: ALL_EVENTS, + DeviceType.HUMIDIFIER: (NOTIFICATION_EVENT_DESC,), + DeviceType.KIMCHI_REFRIGERATOR: (NOTIFICATION_EVENT_DESC,), + DeviceType.MICROWAVE_OVEN: (NOTIFICATION_EVENT_DESC,), + DeviceType.OVEN: (NOTIFICATION_EVENT_DESC,), + DeviceType.REFRIGERATOR: (NOTIFICATION_EVENT_DESC,), + DeviceType.ROBOT_CLEANER: ALL_EVENTS, + DeviceType.STICK_CLEANER: (NOTIFICATION_EVENT_DESC,), + DeviceType.STYLER: ALL_EVENTS, + DeviceType.WASHCOMBO_MAIN: ALL_EVENTS, + DeviceType.WASHCOMBO_MINI: ALL_EVENTS, + DeviceType.WASHER: ALL_EVENTS, + DeviceType.WASHTOWER_DRYER: ALL_EVENTS, + DeviceType.WASHTOWER: ALL_EVENTS, + DeviceType.WASHTOWER_WASHER: ALL_EVENTS, + DeviceType.WINE_CELLAR: (NOTIFICATION_EVENT_DESC,), +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for event platform.""" + entities: list[ThinQEventEntity] = [] + for coordinator in entry.runtime_data.values(): + if ( + descriptions := DEVICE_TYPE_EVENT_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQEventEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx( + description.key, ActiveMode.READ_ONLY + ) + ) + + if entities: + async_add_entities(entities) + + +class ThinQEventEntity(ThinQEntity, EventEntity): + """Represent an thinq event platform.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: EventEntityDescription, + property_id: str, + ) -> None: + """Initialize an event platform.""" + super().__init__(coordinator, entity_description, property_id) + + # For event types. + self._attr_event_types = self.data.options + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + # Handle an event. + if (value := self.data.value) is None or value not in self.event_types: + return + + self._async_handle_update(value) + if self.entity_description.key == ThinQPropertyEx.NOTIFICATION: + self.coordinator.api.update_notification(None) + + _LOGGER.warning( + "[%s:%s] update status:%s, event_types:%s", + self.coordinator.device_name, + self.property_id, + self.data.value, + self.event_types, + ) + + def _async_handle_update(self, value: str) -> None: + """Handle the event.""" + self._trigger_event(value) + self.async_write_ha_state() diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 13b453c23ad..b9843c494bf 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -40,6 +40,14 @@ "default": "mdi:air-filter" } }, + "event": { + "error": { + "default": "mdi:alert-circle-outline" + }, + "notification": { + "default": "mdi:message-badge-outline" + } + }, "number": { "target_temperature": { "default": "mdi:thermometer" diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 8b54d9461c4..1b0fe3f0dde 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -59,9 +59,117 @@ "name": "Fresh air filter" } }, + "event": { + "error": { + "name": "Error", + "state_attributes": { + "event_type": { + "state": { + "block_error": "Cleaning has stopped. Check for obstacles", + "brush_error": "Moving brush has a problem", + "bubble_error": "Bubble error", + "child_lock_active_error": "Child lock", + "cliff_error": "Fall prevention sensor has an error", + "clutch_error": "Clutch error", + "compressor_error": "Compressor error", + "dispensing_error": "Dispensor error", + "door_close_error": "Door closed error", + "door_lock_error": "Door lock error", + "door_open_error": "Door open", + "door_sensor_error": "Door sensor error", + "drainmotor_error": "Drain error", + "dust_full_error": "Dust bin is full and needs to be emptied", + "empty_water_alert_error": "Empty water", + "fan_motor_error": "Fan lock error", + "filter_clogging_error": "Filter error", + "frozen_error": "Freezing detection error", + "heater_circuit_error": "Heater circuit failure", + "high_power_supply_error": "Power supply error", + "high_temperature_detection_error": "High-temperature error", + "inner_lid_open_error": "Lid open error", + "ir_sensor_error": "IR sensor error", + "le_error": "LE error", + "le2_error": "LE2 error", + "left_wheel_error": "Left wheel has a problem", + "locked_motor_error": "Driver motor error", + "mop_error": "Cannot operate properly without the mop attached", + "motor_error": "Motor trouble", + "motor_lock_error": "Motor lock error", + "move_error": "The wheels are not touching the floor", + "need_water_drain": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::empty_water_alert_error%]", + "need_water_replenishment": "Fill water", + "no_battery_error": "Robot cleaner's battery is low", + "no_dust_bin_error": "Dust bin is not installed", + "no_filter_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::filter_clogging_error%]", + "out_of_balance_error": "Out of balance load", + "overfill_error": "Overfill error", + "part_malfunction_error": "AIE error", + "power_code_connection_error": "Power cord connection error", + "power_fail_error": "Power failure", + "right_wheel_error": "Right wheel has a problem", + "stack_error": "Stacking error", + "steam_heat_error": "Steam heater error", + "suction_blocked_error": "Suction motor is clogged", + "temperature_sensor_error": "Thermistor error", + "time_to_run_the_tub_clean_cycle_error": "Tub clean recommendation", + "timeout_error": "Timeout error", + "turbidity_sensor_error": "turbidity sensor error", + "unable_to_lock_error": "Door lock error", + "unbalanced_load_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::out_of_balance_error%]", + "unknown_error": "Product requires attention", + "vibration_sensor_error": "Vibration sensor error", + "water_drain_error": "Water drain error", + "water_leakage_error": "Water leakage problem", + "water_leaks_error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::water_leakage_error%]", + "water_level_sensor_error": "Water sensor error", + "water_supply_error": "Water supply error" + } + } + } + }, + "notification": { + "name": "Notification", + "state_attributes": { + "event_type": { + "state": { + "charging_is_complete": "Charging is completed", + "cleaning_is_complete": "Cycle is finished", + "cleaning_is_completed": "Cleaning is completed", + "cleaning_is_failed": "Cleaning has failed", + "cooking_is_complete": "Turned off", + "door_is_open": "The door is open", + "drying_failed": "An error has occurred in the dryer", + "drying_is_complete": "Drying is completed", + "error_during_cleaning": "Cleaning stopped due to an error", + "error_during_washing": "An error has occurred in the washing machine", + "error_has_occurred": "An error has occurred", + "frozen_is_complete": "Ice plus is done", + "homeguard_is_stopped": "Home guard has stopped", + "lack_of_water": "There is no water in the water tank", + "motion_is_detected": "Photograph is sent as movement is detected during home guard", + "need_to_check_location": "Location check is required", + "pollution_is_high": "Air status is rapidly becoming bad", + "preheating_is_complete": "Preheating is done", + "rinse_is_not_enough": "Add rinse aid for better drying performance", + "salt_refill_is_needed": "Add salt for better softening performance", + "scheduled_cleaning_starts": "Scheduled cleaning starts", + "styling_is_complete": "Styling is completed", + "time_to_change_filter": "It is time to replace the filter", + "time_to_change_water_filter": "You need to replace water filter", + "time_to_clean": "Need to selfcleaning", + "time_to_clean_filter": "It is time to clean the filter", + "timer_is_complete": "Timer has been completed", + "washing_is_complete": "Washing is completed", + "water_is_full": "Water is full", + "water_leak_has_occurred": "The dishwasher has detected a water leak" + } + } + } + } + }, "number": { "target_temperature": { - "name": "Temperature" + "name": "[%key:component::sensor::entity_component::temperature::name%]" }, "target_temperature_for_location": { "name": "{location} temperature" From cd7ac53e0f577c288f1df891ded59e68bb2f7cea Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Mon, 23 Sep 2024 15:55:33 +0900 Subject: [PATCH 03/19] Add mqtt to LG ThinQ integration (#125869) * Add mqtt to LG ThinQ integration * Change file mode * Update for reviews * Change ways to access coordinators in runtime_data --------- Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/__init__.py | 67 ++++++- .../components/lg_thinq/binary_sensor.py | 2 +- homeassistant/components/lg_thinq/const.py | 8 + .../components/lg_thinq/coordinator.py | 12 ++ homeassistant/components/lg_thinq/event.py | 2 +- homeassistant/components/lg_thinq/mqtt.py | 174 ++++++++++++++++++ homeassistant/components/lg_thinq/number.py | 2 +- homeassistant/components/lg_thinq/switch.py | 2 +- 8 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lg_thinq/mqtt.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index c39d07a1d2a..c70811f0fe5 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -3,22 +3,39 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass, field import logging from thinqconnect import ThinQApi, ThinQAPIException from thinqconnect.integration import async_get_ha_bridge_list from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY, Platform +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_COUNTRY, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval -from .const import CONF_CONNECT_CLIENT_ID +from .const import CONF_CONNECT_CLIENT_ID, MQTT_SUBSCRIPTION_INTERVAL from .coordinator import DeviceDataUpdateCoordinator, async_setup_device_coordinator +from .mqtt import ThinQMQTT -type ThinqConfigEntry = ConfigEntry[dict[str, DeviceDataUpdateCoordinator]] + +@dataclass(kw_only=True) +class ThinqData: + """A class that holds runtime data.""" + + coordinators: dict[str, DeviceDataUpdateCoordinator] = field(default_factory=dict) + mqtt_client: ThinQMQTT | None = None + + +type ThinqConfigEntry = ConfigEntry[ThinqData] PLATFORMS = [ Platform.BINARY_SENSOR, @@ -32,7 +49,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ThinqConfigEntry) -> bool: """Set up an entry.""" - entry.runtime_data = {} + entry.runtime_data = ThinqData() access_token = entry.data[CONF_ACCESS_TOKEN] client_id = entry.data[CONF_CONNECT_CLIENT_ID] @@ -51,6 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ThinqConfigEntry) -> boo # Set up all platforms for this device/entry. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Set up MQTT connection. + await async_setup_mqtt(hass, entry, thinq_api, client_id) + # Clean up devices they are no longer in use. async_cleanup_device_registry(hass, entry) @@ -79,14 +99,15 @@ async def async_setup_coordinators( ] task_result = await asyncio.gather(*task_list) for coordinator in task_result: - entry.runtime_data[coordinator.unique_id] = coordinator + entry.runtime_data.coordinators[coordinator.unique_id] = coordinator @callback def async_cleanup_device_registry(hass: HomeAssistant, entry: ThinqConfigEntry) -> None: """Clean up device registry.""" new_device_unique_ids = [ - coordinator.unique_id for coordinator in entry.runtime_data.values() + coordinator.unique_id + for coordinator in entry.runtime_data.coordinators.values() ] device_registry = dr.async_get(hass) existing_entries = dr.async_entries_for_config_entry( @@ -101,6 +122,40 @@ def async_cleanup_device_registry(hass: HomeAssistant, entry: ThinqConfigEntry) _LOGGER.debug("Remove device_registry: device_id=%s", old_entry.id) +async def async_setup_mqtt( + hass: HomeAssistant, entry: ThinqConfigEntry, thinq_api: ThinQApi, client_id: str +) -> None: + """Set up MQTT connection.""" + mqtt_client = ThinQMQTT(hass, thinq_api, client_id, entry.runtime_data.coordinators) + entry.runtime_data.mqtt_client = mqtt_client + + # Try to connect. + result = await mqtt_client.async_connect() + if not result: + _LOGGER.error("Failed to set up mqtt connection") + return + + # Ready to subscribe. + await mqtt_client.async_start_subscribes() + + entry.async_on_unload( + async_track_time_interval( + hass, + mqtt_client.async_refresh_subscribe, + MQTT_SUBSCRIPTION_INTERVAL, + cancel_on_shutdown=True, + ) + ) + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, mqtt_client.async_disconnect + ) + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ThinqConfigEntry) -> bool: """Unload the entry.""" + if entry.runtime_data.mqtt_client: + await entry.runtime_data.mqtt_client.async_disconnect() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lg_thinq/binary_sensor.py b/homeassistant/components/lg_thinq/binary_sensor.py index 596f808ed89..845bf8c3079 100644 --- a/homeassistant/components/lg_thinq/binary_sensor.py +++ b/homeassistant/components/lg_thinq/binary_sensor.py @@ -140,7 +140,7 @@ async def async_setup_entry( ) -> None: """Set up an entry for binary sensor platform.""" entities: list[ThinQBinarySensorEntity] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in entry.runtime_data.coordinators.values(): if ( descriptions := DEVICE_TYPE_BINARY_SENSOR_MAP.get( coordinator.api.device.device_type diff --git a/homeassistant/components/lg_thinq/const.py b/homeassistant/components/lg_thinq/const.py index 09f8c0833df..a65dee715db 100644 --- a/homeassistant/components/lg_thinq/const.py +++ b/homeassistant/components/lg_thinq/const.py @@ -1,5 +1,6 @@ """Constants for LG ThinQ.""" +from datetime import timedelta from typing import Final # Config flow @@ -10,3 +11,10 @@ THINQ_DEFAULT_NAME: Final = "LG ThinQ" THINQ_PAT_URL: Final = "https://connect-pat.lgthinq.com" CLIENT_PREFIX: Final = "home-assistant" CONF_CONNECT_CLIENT_ID: Final = "connect_client_id" + +# MQTT +MQTT_SUBSCRIPTION_INTERVAL: Final = timedelta(days=1) + +# MQTT: Message types +DEVICE_PUSH_MESSAGE: Final = "DEVICE_PUSH" +DEVICE_STATUS_MESSAGE: Final = "DEVICE_STATUS" diff --git a/homeassistant/components/lg_thinq/coordinator.py b/homeassistant/components/lg_thinq/coordinator.py index 5ba77c648a8..0ba859b1228 100644 --- a/homeassistant/components/lg_thinq/coordinator.py +++ b/homeassistant/components/lg_thinq/coordinator.py @@ -57,6 +57,18 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Refresh current status.""" self.async_set_updated_data(self.data) + def handle_update_status(self, status: dict[str, Any]) -> None: + """Handle the status received from the mqtt connection.""" + data = self.api.update_status(status) + if data is not None: + self.async_set_updated_data(data) + + def handle_notification_message(self, message: str | None) -> None: + """Handle the status received from the mqtt connection.""" + data = self.api.update_notification(message) + if data is not None: + self.async_set_updated_data(data) + async def async_setup_device_coordinator( hass: HomeAssistant, ha_bridge: HABridge diff --git a/homeassistant/components/lg_thinq/event.py b/homeassistant/components/lg_thinq/event.py index c9852f968b6..dcbf55eb689 100644 --- a/homeassistant/components/lg_thinq/event.py +++ b/homeassistant/components/lg_thinq/event.py @@ -61,7 +61,7 @@ async def async_setup_entry( ) -> None: """Set up an entry for event platform.""" entities: list[ThinQEventEntity] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in entry.runtime_data.coordinators.values(): if ( descriptions := DEVICE_TYPE_EVENT_MAP.get( coordinator.api.device.device_type diff --git a/homeassistant/components/lg_thinq/mqtt.py b/homeassistant/components/lg_thinq/mqtt.py new file mode 100644 index 00000000000..4d7abab73c8 --- /dev/null +++ b/homeassistant/components/lg_thinq/mqtt.py @@ -0,0 +1,174 @@ +"""Support for LG ThinQ Connect API.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime +import json +import logging +from typing import Any + +from thinqconnect import ThinQApi, ThinQAPIException, ThinQMQTTClient + +from homeassistant.core import Event, HomeAssistant + +from .const import DEVICE_PUSH_MESSAGE, DEVICE_STATUS_MESSAGE +from .coordinator import DeviceDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class ThinQMQTT: + """A class that implements MQTT connection.""" + + def __init__( + self, + hass: HomeAssistant, + thinq_api: ThinQApi, + client_id: str, + coordinators: dict[str, DeviceDataUpdateCoordinator], + ) -> None: + """Initialize a mqtt.""" + self.hass = hass + self.thinq_api = thinq_api + self.client_id = client_id + self.coordinators = coordinators + self.client: ThinQMQTTClient | None = None + + async def async_connect(self) -> bool: + """Create a mqtt client and then try to connect.""" + try: + self.client = await ThinQMQTTClient( + self.thinq_api, self.client_id, self.on_message_received + ) + if self.client is None: + return False + + # Connect to server and create certificate. + return await self.client.async_prepare_mqtt() + except (ThinQAPIException, TypeError, ValueError): + _LOGGER.exception("Failed to connect") + return False + + async def async_disconnect(self, event: Event | None = None) -> None: + """Unregister client and disconnects handlers.""" + await self.async_end_subscribes() + + if self.client is not None: + try: + await self.client.async_disconnect() + except (ThinQAPIException, TypeError, ValueError): + _LOGGER.exception("Failed to disconnect") + + def _get_failed_device_count( + self, results: list[dict | BaseException | None] + ) -> int: + """Check if there exists errors while performing tasks and then return count.""" + # Note that result code '1207' means 'Already subscribed push' + # and is not actually fail. + return sum( + isinstance(result, (TypeError, ValueError)) + or (isinstance(result, ThinQAPIException) and result.code != "1207") + for result in results + ) + + async def async_refresh_subscribe(self, now: datetime | None = None) -> None: + """Update event subscribes.""" + _LOGGER.debug("async_refresh_subscribe: now=%s", now) + + tasks = [ + self.hass.async_create_task( + self.thinq_api.async_post_event_subscribe(coordinator.device_id) + ) + for coordinator in self.coordinators.values() + ] + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + if (count := self._get_failed_device_count(results)) > 0: + _LOGGER.error("Failed to refresh subscription on %s devices", count) + + async def async_start_subscribes(self) -> None: + """Start push/event subscribes.""" + _LOGGER.debug("async_start_subscribes") + + if self.client is None: + _LOGGER.error("Failed to start subscription: No client") + return + + tasks = [ + self.hass.async_create_task( + self.thinq_api.async_post_push_subscribe(coordinator.device_id) + ) + for coordinator in self.coordinators.values() + ] + tasks.extend( + self.hass.async_create_task( + self.thinq_api.async_post_event_subscribe(coordinator.device_id) + ) + for coordinator in self.coordinators.values() + ) + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + if (count := self._get_failed_device_count(results)) > 0: + _LOGGER.error("Failed to start subscription on %s devices", count) + + await self.client.async_connect_mqtt() + + async def async_end_subscribes(self) -> None: + """Start push/event unsubscribes.""" + _LOGGER.debug("async_end_subscribes") + + tasks = [ + self.hass.async_create_task( + self.thinq_api.async_delete_push_subscribe(coordinator.device_id) + ) + for coordinator in self.coordinators.values() + ] + tasks.extend( + self.hass.async_create_task( + self.thinq_api.async_delete_event_subscribe(coordinator.device_id) + ) + for coordinator in self.coordinators.values() + ) + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + if (count := self._get_failed_device_count(results)) > 0: + _LOGGER.error("Failed to end subscription on %s devices", count) + + def on_message_received( + self, + topic: str, + payload: bytes, + dup: bool, + qos: Any, + retain: bool, + **kwargs: dict, + ) -> None: + """Handle the received message that matching the topic.""" + decoded = payload.decode() + try: + message = json.loads(decoded) + except ValueError: + _LOGGER.error("Failed to parse message: payload=%s", decoded) + return + + asyncio.run_coroutine_threadsafe( + self.async_handle_device_event(message), self.hass.loop + ).result() + + async def async_handle_device_event(self, message: dict) -> None: + """Handle received mqtt message.""" + _LOGGER.debug("async_handle_device_event: message=%s", message) + + device_id = message["deviceId"] + coordinator = self.coordinators.get(device_id) + if coordinator is None: + _LOGGER.error("Failed to handle device event: No device") + return + + push_type = message.get("pushType") + + if push_type == DEVICE_STATUS_MESSAGE: + coordinator.handle_update_status(message.get("report", {})) + elif push_type == DEVICE_PUSH_MESSAGE: + coordinator.handle_notification_message(message.get("pushCode")) diff --git a/homeassistant/components/lg_thinq/number.py b/homeassistant/components/lg_thinq/number.py index 4e5e53def6f..bd1ca5ee766 100644 --- a/homeassistant/components/lg_thinq/number.py +++ b/homeassistant/components/lg_thinq/number.py @@ -134,7 +134,7 @@ async def async_setup_entry( ) -> None: """Set up an entry for number platform.""" entities: list[ThinQNumberEntity] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in entry.runtime_data.coordinators.values(): if ( descriptions := DEVICE_TYPE_NUMBER_MAP.get( coordinator.api.device.device_type diff --git a/homeassistant/components/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py index fe78b7813fa..ead274f5753 100644 --- a/homeassistant/components/lg_thinq/switch.py +++ b/homeassistant/components/lg_thinq/switch.py @@ -61,7 +61,7 @@ async def async_setup_entry( ) -> None: """Set up an entry for switch platform.""" entities: list[ThinQSwitchEntity] = [] - for coordinator in entry.runtime_data.values(): + for coordinator in entry.runtime_data.coordinators.values(): if ( descriptions := DEVICE_TYPE_SWITCH_MAP.get( coordinator.api.device.device_type From f09e4316d777e18a2e94794703b6ff52713061fd Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 26 Sep 2024 16:11:26 +0900 Subject: [PATCH 04/19] Add fan platform to LG ThinQ integration (#126712) * Add fan platform to LG ThinQ integration * Turn off on 0 percentage --------- Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/__init__.py | 1 + homeassistant/components/lg_thinq/fan.py | 150 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 homeassistant/components/lg_thinq/fan.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index c70811f0fe5..ac9b455163a 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -40,6 +40,7 @@ type ThinqConfigEntry = ConfigEntry[ThinqData] PLATFORMS = [ Platform.BINARY_SENSOR, Platform.EVENT, + Platform.FAN, Platform.NUMBER, Platform.SWITCH, ] diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py new file mode 100644 index 00000000000..187cc74b3eb --- /dev/null +++ b/homeassistant/components/lg_thinq/fan.py @@ -0,0 +1,150 @@ +"""Support for fan entities.""" + +from __future__ import annotations + +import logging +from typing import Any + +from thinqconnect import DeviceType +from thinqconnect.integration import ExtendedProperty + +from homeassistant.components.fan import ( + FanEntity, + FanEntityDescription, + FanEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + +DEVICE_TYPE_FAN_MAP: dict[DeviceType, tuple[FanEntityDescription, ...]] = { + DeviceType.CEILING_FAN: ( + FanEntityDescription( + key=ExtendedProperty.FAN, + name=None, + ), + ), +} + +FOUR_STEP_SPEEDS = ["low", "mid", "high", "turbo"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for fan platform.""" + entities: list[ThinQFanEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + descriptions := DEVICE_TYPE_FAN_MAP.get(coordinator.api.device.device_type) + ) is not None: + for description in descriptions: + entities.extend( + ThinQFanEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx(description.key) + ) + + if entities: + async_add_entities(entities) + + +class ThinQFanEntity(ThinQEntity, FanEntity): + """Represent a thinq fan platform.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: FanEntityDescription, + property_id: str, + ) -> None: + """Initialize fan platform.""" + super().__init__(coordinator, entity_description, property_id) + + self._ordered_named_fan_speeds = [] + self._attr_supported_features |= FanEntityFeature.SET_SPEED + + if (fan_modes := self.data.fan_modes) is not None: + self._attr_speed_count = len(fan_modes) + if self.speed_count == 4: + self._ordered_named_fan_speeds = FOUR_STEP_SPEEDS + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + # Update power on state. + self._attr_is_on = self.data.is_on + + # Update fan speed. + if ( + self.data.is_on + and (mode := self.data.fan_mode) in self._ordered_named_fan_speeds + ): + self._attr_percentage = ordered_list_item_to_percentage( + self._ordered_named_fan_speeds, mode + ) + else: + self._attr_percentage = 0 + + _LOGGER.debug( + "[%s:%s] update status: %s -> %s (percntage=%s)", + self.coordinator.device_name, + self.property_id, + self.data.is_on, + self.is_on, + self.percentage, + ) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if percentage == 0: + await self.async_turn_off() + return + try: + value = percentage_to_ordered_list_item( + self._ordered_named_fan_speeds, percentage + ) + except ValueError: + _LOGGER.exception("Failed to async_set_percentage") + return + + _LOGGER.debug( + "[%s:%s] async_set_percentage. percntage=%s, value=%s", + self.coordinator.device_name, + self.property_id, + percentage, + value, + ) + await self.async_call_api( + self.coordinator.api.async_set_fan_mode(self.property_id, value) + ) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + _LOGGER.debug( + "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id)) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + _LOGGER.debug( + "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id)) From b1cf70d3a092cbc9a9a5bbb98a25f640a5016b91 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 26 Sep 2024 16:41:07 +0900 Subject: [PATCH 05/19] Bump thinqconnnect to 0.9.8 (#126710) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lg_thinq/manifest.json b/homeassistant/components/lg_thinq/manifest.json index 4b880d2544d..d96f8776873 100644 --- a/homeassistant/components/lg_thinq/manifest.json +++ b/homeassistant/components/lg_thinq/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/lg_thinq/", "iot_class": "cloud_push", "loggers": ["thinqconnect"], - "requirements": ["thinqconnect==0.9.7"] + "requirements": ["thinqconnect==0.9.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ec9f3bccfb..acd79b4d2f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2803,7 +2803,7 @@ thermopro-ble==0.10.0 thingspeak==1.0.0 # homeassistant.components.lg_thinq -thinqconnect==0.9.7 +thinqconnect==0.9.8 # homeassistant.components.tikteck tikteck==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b96e805de1b..a5d03cdb37d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2219,7 +2219,7 @@ thermobeacon-ble==0.7.0 thermopro-ble==0.10.0 # homeassistant.components.lg_thinq -thinqconnect==0.9.7 +thinqconnect==0.9.8 # homeassistant.components.tilt_ble tilt-ble==0.2.3 From 3fbc4c49f35920d25cc5363668febab004f0dc89 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Fri, 27 Sep 2024 18:38:35 +0900 Subject: [PATCH 06/19] Add vacuum platform to LG ThinQ integration (#126711) * Add vacuum to LG ThinQ integration * Update vacuum.py remove constructor * restore constructor * Fix pylint, mypy issues * Update for reviews * Remove constructor --------- Co-authored-by: jangwon.lee Co-authored-by: YunseonPark-LGE <34848373+YunseonPark-LGE@users.noreply.github.com> Co-authored-by: Joostlek --- homeassistant/components/lg_thinq/__init__.py | 1 + homeassistant/components/lg_thinq/vacuum.py | 172 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 homeassistant/components/lg_thinq/vacuum.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index ac9b455163a..508da39d9f7 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -43,6 +43,7 @@ PLATFORMS = [ Platform.FAN, Platform.NUMBER, Platform.SWITCH, + Platform.VACUUM, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lg_thinq/vacuum.py b/homeassistant/components/lg_thinq/vacuum.py new file mode 100644 index 00000000000..138b9ba55bf --- /dev/null +++ b/homeassistant/components/lg_thinq/vacuum.py @@ -0,0 +1,172 @@ +"""Support for vacuum entities.""" + +from __future__ import annotations + +from enum import StrEnum +import logging + +from thinqconnect import DeviceType +from thinqconnect.integration import ExtendedProperty + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_RETURNING, + StateVacuumEntity, + StateVacuumEntityDescription, + VacuumEntityFeature, +) +from homeassistant.const import STATE_IDLE, STATE_PAUSED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ThinqConfigEntry +from .entity import ThinQEntity + +DEVICE_TYPE_VACUUM_MAP: dict[DeviceType, tuple[StateVacuumEntityDescription, ...]] = { + DeviceType.ROBOT_CLEANER: ( + StateVacuumEntityDescription( + key=ExtendedProperty.VACUUM, + name=None, + ), + ), +} + + +class State(StrEnum): + """State of device.""" + + HOMING = "homing" + PAUSE = "pause" + RESUME = "resume" + SLEEP = "sleep" + START = "start" + WAKE_UP = "wake_up" + + +ROBOT_STATUS_TO_HA = { + "charging": STATE_DOCKED, + "diagnosis": STATE_IDLE, + "homing": STATE_RETURNING, + "initializing": STATE_IDLE, + "macrosector": STATE_IDLE, + "monitoring_detecting": STATE_IDLE, + "monitoring_moving": STATE_IDLE, + "monitoring_positioning": STATE_IDLE, + "pause": STATE_PAUSED, + "reservation": STATE_IDLE, + "setdate": STATE_IDLE, + "sleep": STATE_IDLE, + "standby": STATE_IDLE, + "working": STATE_CLEANING, + "error": STATE_ERROR, +} +ROBOT_BATT_TO_HA = { + "moveless": 5, + "dock_level": 5, + "low": 30, + "mid": 50, + "high": 90, + "full": 100, + "over_charge": 100, +} +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for vacuum platform.""" + entities: list[ThinQStateVacuumEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + descriptions := DEVICE_TYPE_VACUUM_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQStateVacuumEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx(description.key) + ) + + if entities: + async_add_entities(entities) + + +class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity): + """Represent a thinq vacuum platform.""" + + _attr_supported_features = ( + VacuumEntityFeature.SEND_COMMAND + | VacuumEntityFeature.STATE + | VacuumEntityFeature.BATTERY + | VacuumEntityFeature.START + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.RETURN_HOME + ) + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + # Update state. + self._attr_state = ROBOT_STATUS_TO_HA[self.data.current_state] + + # Update battery. + if (level := self.data.battery) is not None: + self._attr_battery_level = ( + level if isinstance(level, int) else ROBOT_BATT_TO_HA.get(level, 0) + ) + + _LOGGER.debug( + "[%s:%s] update status: %s -> %s (battery_level=%s)", + self.coordinator.device_name, + self.property_id, + self.data.current_state, + self.state, + self.battery_level, + ) + + async def async_start(self, **kwargs) -> None: + """Start the device.""" + if self.data.current_state == State.SLEEP: + value = State.WAKE_UP + elif self._attr_state == STATE_PAUSED: + value = State.RESUME + else: + value = State.START + + _LOGGER.debug( + "[%s:%s] async_start", self.coordinator.device_name, self.property_id + ) + await self.async_call_api( + self.coordinator.api.async_set_clean_operation_mode(self.property_id, value) + ) + + async def async_pause(self, **kwargs) -> None: + """Pause the device.""" + _LOGGER.debug( + "[%s:%s] async_pause", self.coordinator.device_name, self.property_id + ) + await self.async_call_api( + self.coordinator.api.async_set_clean_operation_mode( + self.property_id, State.PAUSE + ) + ) + + async def async_return_to_base(self, **kwargs) -> None: + """Return device to dock.""" + _LOGGER.debug( + "[%s:%s] async_return_to_base", + self.coordinator.device_name, + self.property_id, + ) + await self.async_call_api( + self.coordinator.api.async_set_clean_operation_mode( + self.property_id, State.HOMING + ) + ) From bc71673ea391d6c1525caca8e09998a34f612290 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 1 Oct 2024 23:23:43 +0900 Subject: [PATCH 07/19] Add sensor platform to LG ThinQ integration (#125231) * Add sensor platform to LG ThinQ integration * Dedup * Dedup * Dedup * Dedup * Dedup * Fix water type enum * Dedup --------- Co-authored-by: jangwon.lee Co-authored-by: Joostlek --- homeassistant/components/lg_thinq/__init__.py | 1 + homeassistant/components/lg_thinq/icons.json | 80 ++++ homeassistant/components/lg_thinq/sensor.py | 328 ++++++++++++++++ .../components/lg_thinq/strings.json | 354 +++++++++++++++++- 4 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lg_thinq/sensor.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index 508da39d9f7..ee5ddfa0435 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -42,6 +42,7 @@ PLATFORMS = [ Platform.EVENT, Platform.FAN, Platform.NUMBER, + Platform.SENSOR, Platform.SWITCH, Platform.VACUUM, ] diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index b9843c494bf..ea6fe78d95e 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -97,6 +97,86 @@ "sleep_timer_relative_hour_to_stop_for_location": { "default": "mdi:bed-clock" } + }, + "sensor": { + "error": { + "default": "mdi:alert-circle-outline" + }, + "current_temperature": { + "default": "mdi:thermometer" + }, + "temperature": { + "default": "mdi:thermometer" + }, + "monitoring_enabled": { + "default": "mdi:monitor-eye" + }, + "growth_mode": { + "default": "mdi:sprout-outline" + }, + "growth_mode_for_location": { + "default": "mdi:sprout-outline" + }, + "wind_volume": { + "default": "mdi:wind-power-outline" + }, + "wind_volume_for_location": { + "default": "mdi:wind-power-outline" + }, + "day_target_temperature": { + "default": "mdi:thermometer" + }, + "day_target_temperature_for_location": { + "default": "mdi:thermometer" + }, + "night_target_temperature": { + "default": "mdi:thermometer" + }, + "night_target_temperature_for_location": { + "default": "mdi:thermometer" + }, + "temperature_state": { + "default": "mdi:thermometer" + }, + "temperature_state_for_location": { + "default": "mdi:thermometer" + }, + "current_state": { + "default": "mdi:list-status" + }, + "current_state_for_location": { + "default": "mdi:list-status" + }, + "fresh_air_filter": { + "default": "mdi:air-filter" + }, + "used_time": { + "default": "mdi:air-filter" + }, + "current_job_mode": { + "default": "mdi:dots-circle" + }, + "current_job_mode_stick_cleaner": { + "default": "mdi:dots-circle" + }, + "personalization_mode": { + "default": "mdi:dots-circle" + }, + "cock_state": { + "default": "mdi:air-filter" + }, + "sterilizing_state": { + "default": "mdi:water-alert-outline" + }, + "water_type": { + "default": "mdi:water" + }, + "target_temperature": { + "default": "mdi:thermometer" + }, + "target_temperature_for_location": { + "default": "mdi:thermometer" + } } } } diff --git a/homeassistant/components/lg_thinq/sensor.py b/homeassistant/components/lg_thinq/sensor.py new file mode 100644 index 00000000000..ca076bed729 --- /dev/null +++ b/homeassistant/components/lg_thinq/sensor.py @@ -0,0 +1,328 @@ +"""Support for sensor entities.""" + +from __future__ import annotations + +import logging + +from thinqconnect import DeviceType +from thinqconnect.devices.const import Property as ThinQProperty +from thinqconnect.integration import ActiveMode, ThinQPropertyEx + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + PERCENTAGE, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + +ERROR_DESC = SensorEntityDescription( + key=ThinQPropertyEx.ERROR, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQPropertyEx.ERROR, +) + +AIR_QUALITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.PM1: SensorEntityDescription( + key=ThinQProperty.PM1, + device_class=SensorDeviceClass.PM1, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ThinQProperty.PM2: SensorEntityDescription( + key=ThinQProperty.PM2, + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ThinQProperty.PM10: SensorEntityDescription( + key=ThinQProperty.PM10, + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ThinQProperty.HUMIDITY: SensorEntityDescription( + key=ThinQProperty.HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ThinQProperty.MONITORING_ENABLED: SensorEntityDescription( + key=ThinQProperty.MONITORING_ENABLED, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.MONITORING_ENABLED, + ), + ThinQProperty.TEMPERATURE: SensorEntityDescription( + key=ThinQProperty.TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + translation_key=ThinQProperty.TEMPERATURE, + ), +} +HUMIDITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.CURRENT_HUMIDITY: SensorEntityDescription( + key=ThinQProperty.CURRENT_HUMIDITY, + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ) +} +JOB_MODE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.CURRENT_JOB_MODE: SensorEntityDescription( + key=ThinQProperty.CURRENT_JOB_MODE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.CURRENT_JOB_MODE, + ), + ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER: SensorEntityDescription( + key=ThinQProperty.CURRENT_JOB_MODE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER, + ), + ThinQProperty.PERSONALIZATION_MODE: SensorEntityDescription( + key=ThinQProperty.PERSONALIZATION_MODE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.PERSONALIZATION_MODE, + ), +} +REFRIGERATION_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.FRESH_AIR_FILTER: SensorEntityDescription( + key=ThinQProperty.FRESH_AIR_FILTER, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.FRESH_AIR_FILTER, + ), +} +RUN_STATE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.CURRENT_STATE: SensorEntityDescription( + key=ThinQProperty.CURRENT_STATE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.CURRENT_STATE, + ), + ThinQProperty.COCK_STATE: SensorEntityDescription( + key=ThinQProperty.COCK_STATE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.COCK_STATE, + ), + ThinQProperty.STERILIZING_STATE: SensorEntityDescription( + key=ThinQProperty.STERILIZING_STATE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.STERILIZING_STATE, + ), + ThinQProperty.GROWTH_MODE: SensorEntityDescription( + key=ThinQProperty.GROWTH_MODE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.GROWTH_MODE, + ), + ThinQProperty.WIND_VOLUME: SensorEntityDescription( + key=ThinQProperty.WIND_VOLUME, + device_class=SensorDeviceClass.WIND_SPEED, + translation_key=ThinQProperty.WIND_VOLUME, + ), +} +TEMPERATURE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.TARGET_TEMPERATURE: SensorEntityDescription( + key=ThinQProperty.TARGET_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + translation_key=ThinQProperty.TARGET_TEMPERATURE, + ), + ThinQProperty.DAY_TARGET_TEMPERATURE: SensorEntityDescription( + key=ThinQProperty.DAY_TARGET_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + translation_key=ThinQProperty.DAY_TARGET_TEMPERATURE, + ), + ThinQProperty.NIGHT_TARGET_TEMPERATURE: SensorEntityDescription( + key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + translation_key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, + ), + ThinQProperty.TEMPERATURE_STATE: SensorEntityDescription( + key=ThinQProperty.TEMPERATURE_STATE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.TEMPERATURE_STATE, + ), + ThinQProperty.CURRENT_TEMPERATURE: SensorEntityDescription( + key=ThinQProperty.CURRENT_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + translation_key=ThinQProperty.CURRENT_TEMPERATURE, + ), +} +WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.WATER_TYPE: SensorEntityDescription( + key=ThinQProperty.WATER_TYPE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.WATER_TYPE, + ), +} +WASHER_SENSORS: tuple[SensorEntityDescription, ...] = ( + ERROR_DESC, + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], +) +DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: ( + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], + ), + DeviceType.AIR_PURIFIER_FAN: ( + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + ), + DeviceType.AIR_PURIFIER: ( + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], + JOB_MODE_SENSOR_DESC[ThinQProperty.PERSONALIZATION_MODE], + ), + DeviceType.COOKTOP: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.DEHUMIDIFIER: ( + JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], + HUMIDITY_SENSOR_DESC[ThinQProperty.CURRENT_HUMIDITY], + ), + DeviceType.DISH_WASHER: ( + ERROR_DESC, + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + ), + DeviceType.DRYER: WASHER_SENSORS, + DeviceType.HOME_BREW: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.HUMIDIFIER: ( + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + ), + DeviceType.KIMCHI_REFRIGERATOR: ( + REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], + SensorEntityDescription( + key=ThinQProperty.TARGET_TEMPERATURE, + translation_key=ThinQProperty.TARGET_TEMPERATURE, + ), + ), + DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.OVEN: ( + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE], + ), + DeviceType.PLANT_CULTIVATOR: ( + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + RUN_STATE_SENSOR_DESC[ThinQProperty.GROWTH_MODE], + RUN_STATE_SENSOR_DESC[ThinQProperty.WIND_VOLUME], + TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE], + TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE], + TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE], + ), + DeviceType.REFRIGERATOR: ( + REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], + ), + DeviceType.ROBOT_CLEANER: ( + ERROR_DESC, + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], + ), + DeviceType.STICK_CLEANER: ( + JOB_MODE_SENSOR_DESC[ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER], + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + ), + DeviceType.STYLER: WASHER_SENSORS, + DeviceType.WASHCOMBO_MAIN: WASHER_SENSORS, + DeviceType.WASHCOMBO_MINI: WASHER_SENSORS, + DeviceType.WASHER: WASHER_SENSORS, + DeviceType.WASHTOWER_DRYER: WASHER_SENSORS, + DeviceType.WASHTOWER: WASHER_SENSORS, + DeviceType.WASHTOWER_WASHER: WASHER_SENSORS, + DeviceType.WATER_HEATER: ( + TEMPERATURE_SENSOR_DESC[ThinQProperty.CURRENT_TEMPERATURE], + ), + DeviceType.WATER_PURIFIER: ( + RUN_STATE_SENSOR_DESC[ThinQProperty.COCK_STATE], + RUN_STATE_SENSOR_DESC[ThinQProperty.STERILIZING_STATE], + WATER_INFO_SENSOR_DESC[ThinQProperty.WATER_TYPE], + ), +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for sensor platform.""" + entities: list[ThinQSensorEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + descriptions := DEVICE_TYPE_SENSOR_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQSensorEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx( + description.key, ActiveMode.READ_ONLY + ) + ) + + if entities: + async_add_entities(entities) + + +class ThinQSensorEntity(ThinQEntity, SensorEntity): + """Represent a thinq sensor platform.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: SensorEntityDescription, + property_id: str, + ) -> None: + """Initialize a sensor entity.""" + super().__init__(coordinator, entity_description, property_id) + + if entity_description.device_class == SensorDeviceClass.ENUM: + self._attr_options = self.data.options + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + self._attr_native_value = self.data.value + + if (data_unit := self._get_unit_of_measurement(self.data.unit)) is not None: + # For different from description's unit + self._attr_native_unit_of_measurement = data_unit + + _LOGGER.debug( + "[%s:%s] update status: %s, options:%s, unit:%s", + self.coordinator.device_name, + self.property_id, + self.data.value, + self.options, + self.native_unit_of_measurement, + ) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 1b0fe3f0dde..6ccbdf5225d 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -181,7 +181,7 @@ "name": "Fan" }, "lamp_brightness": { - "name": "Light" + "name": "[%key:component::lg_thinq::entity::number::light_status::name%]" }, "wind_temperature": { "name": "Wind temperature" @@ -216,6 +216,358 @@ "sleep_timer_relative_hour_to_stop_for_location": { "name": "{location} sleep timer" } + }, + "sensor": { + "error": { + "name": "Error" + }, + "current_temperature": { + "name": "Current temperature" + }, + "monitoring_enabled": { + "name": "Air quality sensor", + "state": { + "on_working": "Turns on with product", + "always": "Always on" + } + }, + "growth_mode": { + "name": "Mode", + "state": { + "standard": "Auto", + "ext_leaf": "Vegetables", + "ext_herb": "Herbs", + "ext_flower": "Flowers", + "ext_expert": "Custom growing mode" + } + }, + "growth_mode_for_location": { + "name": "{location} mode", + "state": { + "standard": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "ext_leaf": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_leaf%]", + "ext_herb": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_herb%]", + "ext_flower": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_flower%]", + "ext_expert": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_expert%]" + } + }, + "wind_volume_for_location": { + "name": "{location} wind speed" + }, + "day_target_temperature": { + "name": "Day growth temperature" + }, + "day_target_temperature_for_location": { + "name": "{location} day growth temperature" + }, + "night_target_temperature": { + "name": "Night growth temperature" + }, + "night_target_temperature_for_location": { + "name": "{location} night growth temperature" + }, + "temperature_state": { + "name": "[%key:component::sensor::entity_component::temperature::name%]", + "state": { + "high": "High", + "normal": "Good", + "low": "Low" + } + }, + "temperature_state_for_location": { + "name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]", + "state": { + "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", + "normal": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::normal%]", + "low": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::low%]" + } + }, + "current_state": { + "name": "Current status", + "state": { + "add_drain": "Filling", + "as_pop_up": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]", + "cancel": "Cancel", + "carbonation": "Carbonation", + "change_condition": "Settings Change", + "charging": "Charging", + "charging_complete": "Charging completed", + "checking_turbidity": "Detecting soil level", + "cleaning": "Cleaning", + "cleaning_is_done": "Cleaning is done", + "complete": "Done", + "cook": "Cooking", + "cook_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]", + "cooking_in_progress": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]", + "cool_down": "Cool down", + "cooling": "Cooling", + "detecting": "Detecting", + "detergent_amount": "Providing the info about the amount of detergent", + "diagnosis": "Smart diagnosis is in progress", + "dispensing": "Auto dispensing", + "display_loadsize": "Load size", + "done": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]", + "drying": "Drying", + "during_aging": "Aging", + "during_fermentation": "Fermentation", + "end": "Finished", + "end_cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]", + "error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]", + "extracting_capsule": "Capsule brewing", + "extraction_mode": "Storing", + "firmware": "Updating firmware", + "fota": "Updating", + "frozen_prevent_initial": "Freeze protection standby", + "frozen_prevent_running": "Freeze protection in progress", + "frozen_prevent_pause": "Freeze protection paused", + "homing": "Moving", + "initial": "[%key:common::state::standby%]", + "initializing": "[%key:common::state::standby%]", + "lock": "Control lock", + "macrosector": "Remote is in use", + "melting": "Wort dissolving", + "monitoring_detecting": "HomeGuard is active", + "monitoring_moving": "Going to the starting point", + "monitoring_positioning": "Setting homeguard start point", + "night_dry": "Night dry", + "oven_setting": "Cooktop connected", + "pause": "[%key:common::state::paused%]", + "paused": "[%key:common::state::paused%]", + "power_fail": "Power fail", + "power_on": "[%key:common::state::on%]", + "power_off": "[%key:common::state::off%]", + "preference": "Setting", + "preheat": "Preheating", + "preheat_complete": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]", + "preheating": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]", + "preheating_is_done": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]", + "prepareing_fermentation": "Preparing now", + "presteam": "Ready to steam", + "prewash": "Prewashing", + "proofing": "Proofing", + "refreshing": "Refreshing", + "reservation": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]", + "reserved": "Delay set", + "rinse_hold": "Waiting to rinse", + "rinsing": "Rinsing", + "running": "Running", + "running_end": "Complete", + "setdate": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]", + "shoes_module": "Drying shoes", + "sleep": "In sleep mode", + "smart_grid_run": "Running smart grid", + "soaking": "Soak", + "softening": "Softener", + "spinning": "Spinning", + "stay": "Refresh", + "standby": "[%key:common::state::standby%]", + "steam": "Refresh", + "steam_softening": "Steam softening", + "sterilize": "Sterilize", + "temperature_stabilization": "Temperature adjusting", + "working": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", + "wrinkle_care": "Wrinkle care" + } + }, + "current_state_for_location": { + "name": "{location} current status", + "state": { + "add_drain": "[%key:component::lg_thinq::entity::sensor::current_state::state::add_drain%]", + "as_pop_up": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]", + "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]", + "carbonation": "[%key:component::lg_thinq::entity::sensor::current_state::state::carbonation%]", + "change_condition": "[%key:component::lg_thinq::entity::sensor::current_state::state::change_condition%]", + "charging": "[%key:component::lg_thinq::entity::sensor::current_state::state::charging%]", + "charging_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::charging_complete%]", + "checking_turbidity": "[%key:component::lg_thinq::entity::sensor::current_state::state::checking_turbidity%]", + "cleaning": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", + "cleaning_is_done": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning_is_done%]", + "complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]", + "cook": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]", + "cook_complete": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]", + "cooking_in_progress": "[%key:component::lg_thinq::entity::sensor::current_state::state::cook%]", + "cool_down": "[%key:component::lg_thinq::entity::sensor::current_state::state::cool_down%]", + "cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::cooling%]", + "detecting": "[%key:component::lg_thinq::entity::sensor::current_state::state::detecting%]", + "detergent_amount": "[%key:component::lg_thinq::entity::sensor::current_state::state::detergent_amount%]", + "diagnosis": "[%key:component::lg_thinq::entity::sensor::current_state::state::diagnosis%]", + "dispensing": "[%key:component::lg_thinq::entity::sensor::current_state::state::dispensing%]", + "display_loadsize": "[%key:component::lg_thinq::entity::sensor::current_state::state::display_loadsize%]", + "done": "[%key:component::lg_thinq::entity::sensor::current_state::state::complete%]", + "drying": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]", + "during_aging": "[%key:component::lg_thinq::entity::sensor::current_state::state::during_aging%]", + "during_fermentation": "[%key:component::lg_thinq::entity::sensor::current_state::state::during_fermentation%]", + "end": "[%key:component::lg_thinq::entity::sensor::current_state::state::end%]", + "end_cooling": "[%key:component::lg_thinq::entity::sensor::current_state::state::drying%]", + "error": "[%key:component::lg_thinq::entity::event::error::state_attributes::event_type::state::unknown_error%]", + "extracting_capsule": "[%key:component::lg_thinq::entity::sensor::current_state::state::extracting_capsule%]", + "extraction_mode": "[%key:component::lg_thinq::entity::sensor::current_state::state::extraction_mode%]", + "firmware": "[%key:component::lg_thinq::entity::sensor::current_state::state::firmware%]", + "fota": "[%key:component::lg_thinq::entity::sensor::current_state::state::fota%]", + "frozen_prevent_initial": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_initial%]", + "frozen_prevent_running": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_running%]", + "frozen_prevent_pause": "[%key:component::lg_thinq::entity::sensor::current_state::state::frozen_prevent_pause%]", + "homing": "[%key:component::lg_thinq::entity::sensor::current_state::state::homing%]", + "initial": "[%key:common::state::standby%]", + "initializing": "[%key:common::state::standby%]", + "lock": "[%key:component::lg_thinq::entity::sensor::current_state::state::lock%]", + "macrosector": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]", + "melting": "[%key:component::lg_thinq::entity::sensor::current_state::state::melting%]", + "monitoring_detecting": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_detecting%]", + "monitoring_moving": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_moving%]", + "monitoring_positioning": "[%key:component::lg_thinq::entity::sensor::current_state::state::monitoring_positioning%]", + "night_dry": "[%key:component::lg_thinq::entity::sensor::current_state::state::night_dry%]", + "oven_setting": "[%key:component::lg_thinq::entity::sensor::current_state::state::oven_setting%]", + "pause": "[%key:common::state::paused%]", + "paused": "[%key:common::state::paused%]", + "power_fail": "[%key:component::lg_thinq::entity::sensor::current_state::state::power_fail%]", + "power_on": "[%key:common::state::on%]", + "power_off": "[%key:common::state::off%]", + "preference": "[%key:component::lg_thinq::entity::sensor::current_state::state::preference%]", + "preheat": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]", + "preheat_complete": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]", + "preheating": "[%key:component::lg_thinq::entity::sensor::current_state::state::preheat%]", + "preheating_is_done": "[%key:component::lg_thinq::entity::event::notification::state_attributes::event_type::state::preheating_is_complete%]", + "prepareing_fermentation": "[%key:component::lg_thinq::entity::sensor::current_state::state::prepareing_fermentation%]", + "presteam": "[%key:component::lg_thinq::entity::sensor::current_state::state::presteam%]", + "prewash": "[%key:component::lg_thinq::entity::sensor::current_state::state::prewash%]", + "proofing": "[%key:component::lg_thinq::entity::sensor::current_state::state::proofing%]", + "refreshing": "[%key:component::lg_thinq::entity::sensor::current_state::state::refreshing%]", + "reservation": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]", + "reserved": "[%key:component::lg_thinq::entity::sensor::current_state::state::reserved%]", + "rinse_hold": "[%key:component::lg_thinq::entity::sensor::current_state::state::rinse_hold%]", + "rinsing": "[%key:component::lg_thinq::entity::sensor::current_state::state::rinsing%]", + "running": "[%key:component::lg_thinq::entity::sensor::current_state::state::running%]", + "running_end": "[%key:component::lg_thinq::entity::sensor::current_state::state::running_end%]", + "setdate": "[%key:component::lg_thinq::entity::sensor::current_state::state::macrosector%]", + "shoes_module": "[%key:component::lg_thinq::entity::sensor::current_state::state::shoes_module%]", + "sleep": "[%key:component::lg_thinq::entity::sensor::current_state::state::sleep%]", + "smart_grid_run": "[%key:component::lg_thinq::entity::sensor::current_state::state::smart_grid_run%]", + "soaking": "[%key:component::lg_thinq::entity::sensor::current_state::state::soaking%]", + "softening": "[%key:component::lg_thinq::entity::sensor::current_state::state::softening%]", + "spinning": "[%key:component::lg_thinq::entity::sensor::current_state::state::spinning%]", + "stay": "[%key:component::lg_thinq::entity::sensor::current_state::state::stay%]", + "standby": "[%key:common::state::standby%]", + "steam": "[%key:component::lg_thinq::entity::sensor::current_state::state::steam%]", + "steam_softening": "[%key:component::lg_thinq::entity::sensor::current_state::state::steam_softening%]", + "sterilize": "[%key:component::lg_thinq::entity::sensor::current_state::state::sterilize%]", + "temperature_stabilization": "[%key:component::lg_thinq::entity::sensor::current_state::state::temperature_stabilization%]", + "working": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", + "wrinkle_care": "[%key:component::lg_thinq::entity::sensor::current_state::state::wrinkle_care%]" + } + }, + "fresh_air_filter": { + "name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]", + "state": { + "off": "[%key:common::state::off%]", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "power": "Power", + "replace": "Replace filter", + "smart_power": "Smart safe storage", + "smart_off": "[%key:common::state::off%]", + "smart_on": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]" + } + }, + "used_time": { + "name": "Water filter used" + }, + "current_job_mode": { + "name": "Operating mode", + "state": { + "air_clean": "Purify", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "clothes_dry": "Laundry", + "edge": "Edge cleaning", + "heat_pump": "Heat pump", + "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", + "intensive_dry": "Spot", + "macro": "Custom mode", + "mop": "Mop", + "normal": "Normal", + "off": "[%key:common::state::off%]", + "quiet_humidity": "Silent", + "rapid_humidity": "Jet", + "sector_base": "Cell by cell", + "select": "My space", + "smart_humidity": "Smart", + "spot": "Spiral spot mode", + "turbo": "Turbo", + "vacation": "Vacation", + "zigzag": "Zigzag" + } + }, + "current_job_mode_stick_cleaner": { + "name": "Operating mode", + "state": { + "auto": "Low power", + "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", + "mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]", + "normal": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::normal%]", + "off": "[%key:common::state::off%]", + "turbo": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::turbo%]" + } + }, + "personalization_mode": { + "name": "Personal mode", + "state": { + "auto_inside": "Auto mode", + "sleep": "Sleep mode", + "baby": "Baby care mode", + "sick_house": "New Home mode", + "auto_outside": "Interlocking mode", + "pet": "Pet mode", + "cooking": "Cooking mode", + "smoke": "Smoke mode", + "exercise": "Exercise mode", + "others": "Others" + } + }, + "cock_state": { + "name": "UVnano", + "state": { + "cleaning": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", + "normal": "[%key:common::state::standby%]" + } + }, + "sterilizing_state": { + "name": "High-temp sterilization", + "state": { + "off": "[%key:common::state::off%]", + "on": "Sterilizing", + "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]" + } + }, + "water_type": { + "name": "Type" + }, + "target_temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]", + "state": { + "kimchi": "Kimchi", + "off": "[%key:common::state::off%]", + "freezer": "Freezer", + "fridge": "Fridge", + "storage": "Storage", + "meat_fish": "Meat/Fish", + "rice_grain": "Rice/Grain", + "vegetable_fruit": "Vege/Fruit", + "temperature_number": "Number" + } + }, + "target_temperature_for_location": { + "name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]", + "state": { + "kimchi": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::kimchi%]", + "off": "[%key:common::state::off%]", + "freezer": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::freezer%]", + "fridge": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::fridge%]", + "storage": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::storage%]", + "meat_fish": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::meat_fish%]", + "rice_grain": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::rice_grain%]", + "vegetable_fruit": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::vegetable_fruit%]", + "temperature_number": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::temperature_number%]" + } + } } } } From 6992661554d49cf974eea48549677ec605878312 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Wed, 2 Oct 2024 00:13:18 +0900 Subject: [PATCH 08/19] Add climate platform to LG ThinQ integration (#126241) * Add climate platform to LG ThinQ integration * Update for reviews * Modify update_status --------- Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/__init__.py | 1 + homeassistant/components/lg_thinq/climate.py | 334 ++++++++++++++++++ .../components/lg_thinq/strings.json | 23 ++ 3 files changed, 358 insertions(+) create mode 100644 homeassistant/components/lg_thinq/climate.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index ee5ddfa0435..6ca74cda8c0 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -39,6 +39,7 @@ type ThinqConfigEntry = ConfigEntry[ThinqData] PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.CLIMATE, Platform.EVENT, Platform.FAN, Platform.NUMBER, diff --git a/homeassistant/components/lg_thinq/climate.py b/homeassistant/components/lg_thinq/climate.py new file mode 100644 index 00000000000..9ead57ab7b0 --- /dev/null +++ b/homeassistant/components/lg_thinq/climate.py @@ -0,0 +1,334 @@ +"""Support for climate entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + +from thinqconnect import DeviceType +from thinqconnect.integration import ExtendedProperty + +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_OFF, + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.temperature import display_temp + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + + +@dataclass(frozen=True, kw_only=True) +class ThinQClimateEntityDescription(ClimateEntityDescription): + """Describes ThinQ climate entity.""" + + min_temp: float | None = None + max_temp: float | None = None + step: float | None = None + + +DEVIE_TYPE_CLIMATE_MAP: dict[DeviceType, tuple[ThinQClimateEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: ( + ThinQClimateEntityDescription( + key=ExtendedProperty.CLIMATE_AIR_CONDITIONER, + name=None, + translation_key=ExtendedProperty.CLIMATE_AIR_CONDITIONER, + ), + ), + DeviceType.SYSTEM_BOILER: ( + ThinQClimateEntityDescription( + key=ExtendedProperty.CLIMATE_SYSTEM_BOILER, + name=None, + min_temp=16, + max_temp=30, + step=1, + ), + ), +} + +STR_TO_HVAC: dict[str, HVACMode] = { + "air_dry": HVACMode.DRY, + "auto": HVACMode.AUTO, + "cool": HVACMode.COOL, + "fan": HVACMode.FAN_ONLY, + "heat": HVACMode.HEAT, +} + +HVAC_TO_STR: dict[HVACMode, str] = { + HVACMode.AUTO: "auto", + HVACMode.COOL: "cool", + HVACMode.DRY: "air_dry", + HVACMode.FAN_ONLY: "fan", + HVACMode.HEAT: "heat", +} + +THINQ_PRESET_MODE: list[str] = ["air_clean", "aroma", "energy_saving"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for climate platform.""" + entities: list[ThinQClimateEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + descriptions := DEVIE_TYPE_CLIMATE_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQClimateEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx(description.key) + ) + + if entities: + async_add_entities(entities) + + +class ThinQClimateEntity(ThinQEntity, ClimateEntity): + """Represent a thinq climate platform.""" + + entity_description: ThinQClimateEntityDescription + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: ThinQClimateEntityDescription, + property_id: str, + ) -> None: + """Initialize a climate entity.""" + super().__init__(coordinator, entity_description, property_id) + + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) + self._attr_hvac_modes = [HVACMode.OFF] + self._attr_hvac_mode = HVACMode.OFF + self._attr_preset_modes = [] + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._requested_hvac_mode: str | None = None + + # Set up HVAC modes. + for mode in self.data.hvac_modes: + if mode in STR_TO_HVAC: + self._attr_hvac_modes.append(STR_TO_HVAC[mode]) + elif mode in THINQ_PRESET_MODE: + self._attr_preset_modes.append(mode) + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + + # Set up fan modes. + self._attr_fan_modes = self.data.fan_modes + if self.fan_modes: + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + + # Supports target temperature range. + if self.data.support_temperature_range: + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + # Update fan, hvac and preset mode. + if self.data.is_on: + if self.supported_features & ClimateEntityFeature.FAN_MODE: + self._attr_fan_mode = self.data.fan_mode + + hvac_mode = self._requested_hvac_mode or self.data.hvac_mode + if hvac_mode in STR_TO_HVAC: + self._attr_hvac_mode = STR_TO_HVAC.get(hvac_mode) + self._attr_preset_mode = None + elif hvac_mode in THINQ_PRESET_MODE: + self._attr_preset_mode = hvac_mode + else: + if self.supported_features & ClimateEntityFeature.FAN_MODE: + self._attr_fan_mode = FAN_OFF + + self._attr_hvac_mode = HVACMode.OFF + self._attr_preset_mode = None + + self.reset_requested_hvac_mode() + self._attr_current_humidity = self.data.humidity + self._attr_current_temperature = self.data.current_temp + + if (max_temp := self.entity_description.max_temp) is not None or ( + max_temp := self.data.max + ) is not None: + self._attr_max_temp = max_temp + if (min_temp := self.entity_description.min_temp) is not None or ( + min_temp := self.data.min + ) is not None: + self._attr_min_temp = min_temp + if (step := self.entity_description.step) is not None or ( + step := self.data.step + ) is not None: + self._attr_target_temperature_step = step + + # Update target temperatures. + if ( + self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + and self.hvac_mode == HVACMode.AUTO + ): + self._attr_target_temperature = None + self._attr_target_temperature_high = self.data.target_temp_high + self._attr_target_temperature_low = self.data.target_temp_low + else: + self._attr_target_temperature = self.data.target_temp + self._attr_target_temperature_high = None + self._attr_target_temperature_low = None + + _LOGGER.debug( + "[%s:%s] update status: %s/%s -> %s/%s, hvac:%s, unit:%s, step:%s", + self.coordinator.device_name, + self.property_id, + self.data.current_temp, + self.data.target_temp, + self.current_temperature, + self.target_temperature, + self.hvac_mode, + self.temperature_unit, + self.target_temperature_step, + ) + + def reset_requested_hvac_mode(self) -> None: + """Cancel request to set hvac mode.""" + self._requested_hvac_mode = None + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + _LOGGER.debug( + "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id)) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + _LOGGER.debug( + "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id + ) + await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id)) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + return + + # If device is off, turn on first. + if not self.data.is_on: + await self.async_turn_on() + + # When we request hvac mode while turning on the device, the previously set + # hvac mode is displayed first and then switches to the requested hvac mode. + # To prevent this, set the requested hvac mode here so that it will be set + # immediately on the next update. + self._requested_hvac_mode = HVAC_TO_STR.get(hvac_mode) + + _LOGGER.debug( + "[%s:%s] async_set_hvac_mode: %s", + self.coordinator.device_name, + self.property_id, + hvac_mode, + ) + await self.async_call_api( + self.coordinator.api.async_set_hvac_mode( + self.property_id, self._requested_hvac_mode + ), + self.reset_requested_hvac_mode, + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + _LOGGER.debug( + "[%s:%s] async_set_preset_mode: %s", + self.coordinator.device_name, + self.property_id, + preset_mode, + ) + await self.async_call_api( + self.coordinator.api.async_set_hvac_mode(self.property_id, preset_mode) + ) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + _LOGGER.debug( + "[%s:%s] async_set_fan_mode: %s", + self.coordinator.device_name, + self.property_id, + fan_mode, + ) + await self.async_call_api( + self.coordinator.api.async_set_fan_mode(self.property_id, fan_mode) + ) + + def _round_by_step(self, temperature: float) -> float: + """Round the value by step.""" + if ( + target_temp := display_temp( + self.coordinator.hass, + temperature, + self.coordinator.hass.config.units.temperature_unit, + self.target_temperature_step or 1, + ) + ) is not None: + return target_temp + + return temperature + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + _LOGGER.debug( + "[%s:%s] async_set_temperature: %s", + self.coordinator.device_name, + self.property_id, + kwargs, + ) + + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: + if ( + target_temp := self._round_by_step(temperature) + ) != self.target_temperature: + await self.async_call_api( + self.coordinator.api.async_set_target_temperature( + self.property_id, target_temp + ) + ) + + if (temperature_low := kwargs.get(ATTR_TARGET_TEMP_LOW)) is not None: + if ( + target_temp_low := self._round_by_step(temperature_low) + ) != self.target_temperature_low: + await self.async_call_api( + self.coordinator.api.async_set_target_temperature_low( + self.property_id, target_temp_low + ) + ) + + if (temperature_high := kwargs.get(ATTR_TARGET_TEMP_HIGH)) is not None: + if ( + target_temp_high := self._round_by_step(temperature_high) + ) != self.target_temperature_high: + await self.async_call_api( + self.coordinator.api.async_set_target_temperature_high( + self.property_id, target_temp_high + ) + ) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 6ccbdf5225d..45b088cb3f4 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -59,6 +59,29 @@ "name": "Fresh air filter" } }, + "climate": { + "climate_air_conditioner": { + "state_attributes": { + "fan_mode": { + "state": { + "slow": "Slow", + "low": "Low", + "mid": "Medium", + "high": "High", + "power": "Power", + "auto": "Auto" + } + }, + "preset_mode": { + "state": { + "air_clean": "Air purify", + "aroma": "Aroma", + "energy_saving": "Energy saving" + } + } + } + } + }, "event": { "error": { "name": "Error", From 62ecd50910cfb6d8696c8f6550b8b2f75542f301 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 7 Oct 2024 09:05:03 +0200 Subject: [PATCH 09/19] Add testing framework for LG ThinQ (#127237) * Add basic testing framework to LG ThinQ * Add basic testing framework to LG ThinQ * Add basic testing framework to LG ThinQ --- tests/components/lg_thinq/__init__.py | 14 +- tests/components/lg_thinq/conftest.py | 34 ++- .../fixtures/air_conditioner/device.json | 9 + .../fixtures/air_conditioner/profile.json | 154 +++++++++++++ .../fixtures/air_conditioner/status.json | 43 ++++ .../lg_thinq/snapshots/test_climate.ambr | 86 ++++++++ .../lg_thinq/snapshots/test_event.ambr | 55 +++++ .../lg_thinq/snapshots/test_number.ambr | 113 ++++++++++ .../lg_thinq/snapshots/test_sensor.ambr | 205 ++++++++++++++++++ tests/components/lg_thinq/test_climate.py | 29 +++ tests/components/lg_thinq/test_config_flow.py | 5 +- tests/components/lg_thinq/test_event.py | 29 +++ tests/components/lg_thinq/test_init.py | 26 +++ tests/components/lg_thinq/test_number.py | 29 +++ tests/components/lg_thinq/test_sensor.py | 29 +++ 15 files changed, 853 insertions(+), 7 deletions(-) create mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/device.json create mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/profile.json create mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/status.json create mode 100644 tests/components/lg_thinq/snapshots/test_climate.ambr create mode 100644 tests/components/lg_thinq/snapshots/test_event.ambr create mode 100644 tests/components/lg_thinq/snapshots/test_number.ambr create mode 100644 tests/components/lg_thinq/snapshots/test_sensor.ambr create mode 100644 tests/components/lg_thinq/test_climate.py create mode 100644 tests/components/lg_thinq/test_event.py create mode 100644 tests/components/lg_thinq/test_init.py create mode 100644 tests/components/lg_thinq/test_number.py create mode 100644 tests/components/lg_thinq/test_sensor.py diff --git a/tests/components/lg_thinq/__init__.py b/tests/components/lg_thinq/__init__.py index 68ffb960f71..a5ba55ab1c9 100644 --- a/tests/components/lg_thinq/__init__.py +++ b/tests/components/lg_thinq/__init__.py @@ -1 +1,13 @@ -"""Tests for the lgthinq integration.""" +"""Tests for the LG ThinQ integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/lg_thinq/conftest.py b/tests/components/lg_thinq/conftest.py index cae2de61fa4..05cb3164137 100644 --- a/tests/components/lg_thinq/conftest.py +++ b/tests/components/lg_thinq/conftest.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY from .const import MOCK_CONNECT_CLIENT_ID, MOCK_COUNTRY, MOCK_PAT, MOCK_UUID -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture def mock_thinq_api_response( @@ -45,6 +45,15 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.lg_thinq.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + @pytest.fixture def mock_uuid() -> Generator[AsyncMock]: """Mock a uuid.""" @@ -59,22 +68,37 @@ def mock_uuid() -> Generator[AsyncMock]: @pytest.fixture -def mock_thinq_api() -> Generator[AsyncMock]: +def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]: """Mock a thinq api.""" with ( - patch("thinqconnect.ThinQApi", autospec=True) as mock_api, + patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api, patch( "homeassistant.components.lg_thinq.config_flow.ThinQApi", new=mock_api, ), ): thinq_api = mock_api.return_value - thinq_api.async_get_device_list = AsyncMock( - return_value=mock_thinq_api_response(status=200, body={}) + thinq_api.async_get_device_list.return_value = [ + load_json_object_fixture("air_conditioner/device.json", DOMAIN) + ] + thinq_api.async_get_device_profile.return_value = load_json_object_fixture( + "air_conditioner/profile.json", DOMAIN + ) + thinq_api.async_get_device_status.return_value = load_json_object_fixture( + "air_conditioner/status.json", DOMAIN ) yield thinq_api +@pytest.fixture +def mock_thinq_mqtt_client() -> Generator[AsyncMock]: + """Mock a thinq api.""" + with patch( + "homeassistant.components.lg_thinq.mqtt.ThinQMQTTClient", autospec=True + ) as mock_api: + yield mock_api + + @pytest.fixture def mock_invalid_thinq_api(mock_thinq_api: AsyncMock) -> AsyncMock: """Mock an invalid thinq api.""" diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/device.json b/tests/components/lg_thinq/fixtures/air_conditioner/device.json new file mode 100644 index 00000000000..fb931c69929 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/air_conditioner/device.json @@ -0,0 +1,9 @@ +{ + "deviceId": "MW2-2E247F93-B570-46A6-B827-920E9E10F966", + "deviceInfo": { + "deviceType": "DEVICE_AIR_CONDITIONER", + "modelName": "PAC_910604_WW", + "alias": "Test air conditioner", + "reportable": true + } +} diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/profile.json b/tests/components/lg_thinq/fixtures/air_conditioner/profile.json new file mode 100644 index 00000000000..0d45dc5c9f4 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/air_conditioner/profile.json @@ -0,0 +1,154 @@ +{ + "notification": { + "push": ["WATER_IS_FULL"] + }, + "property": { + "airConJobMode": { + "currentJobMode": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["AIR_CLEAN", "COOL", "AIR_DRY"], + "w": ["AIR_CLEAN", "COOL", "AIR_DRY"] + } + } + }, + "airFlow": { + "windStrength": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["LOW", "HIGH", "MID"], + "w": ["LOW", "HIGH", "MID"] + } + } + }, + "airQualitySensor": { + "PM1": { + "mode": ["r"], + "type": "number" + }, + "PM10": { + "mode": ["r"], + "type": "number" + }, + "PM2": { + "mode": ["r"], + "type": "number" + }, + "humidity": { + "mode": ["r"], + "type": "number" + }, + "monitoringEnabled": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["ON_WORKING", "ALWAYS"], + "w": ["ON_WORKING", "ALWAYS"] + } + }, + "oder": { + "mode": ["r"], + "type": "number" + }, + "totalPollution": { + "mode": ["r"], + "type": "number" + } + }, + "operation": { + "airCleanOperationMode": { + "mode": ["w"], + "type": "enum", + "value": { + "w": ["START", "STOP"] + } + }, + "airConOperationMode": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["POWER_ON", "POWER_OFF"], + "w": ["POWER_ON", "POWER_OFF"] + } + } + }, + "powerSave": { + "powerSaveEnabled": { + "mode": ["r", "w"], + "type": "boolean", + "value": { + "r": [false, true], + "w": [false, true] + } + } + }, + "temperature": { + "coolTargetTemperature": { + "mode": ["w"], + "type": "range", + "value": { + "w": { + "max": 30, + "min": 18, + "step": 1 + } + } + }, + "currentTemperature": { + "mode": ["r"], + "type": "number" + }, + "targetTemperature": { + "mode": ["r", "w"], + "type": "range", + "value": { + "r": { + "max": 30, + "min": 18, + "step": 1 + }, + "w": { + "max": 30, + "min": 18, + "step": 1 + } + } + }, + "unit": { + "mode": ["r"], + "type": "enum", + "value": { + "r": ["C", "F"] + } + } + }, + "timer": { + "relativeHourToStart": { + "mode": ["r", "w"], + "type": "number" + }, + "relativeHourToStop": { + "mode": ["r", "w"], + "type": "number" + }, + "relativeMinuteToStart": { + "mode": ["r", "w"], + "type": "number" + }, + "relativeMinuteToStop": { + "mode": ["r", "w"], + "type": "number" + }, + "absoluteHourToStart": { + "mode": ["r", "w"], + "type": "number" + }, + "absoluteMinuteToStart": { + "mode": ["r", "w"], + "type": "number" + } + } + } +} diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/status.json b/tests/components/lg_thinq/fixtures/air_conditioner/status.json new file mode 100644 index 00000000000..90d15d1ae16 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/air_conditioner/status.json @@ -0,0 +1,43 @@ +{ + "airConJobMode": { + "currentJobMode": "COOL" + }, + "airFlow": { + "windStrength": "MID" + }, + "airQualitySensor": { + "PM1": 12, + "PM10": 7, + "PM2": 24, + "humidity": 40, + "monitoringEnabled": "ON_WORKING", + "totalPollution": 3, + "totalPollutionLevel": "GOOD" + }, + "filterInfo": { + "filterLifetime": 540, + "usedTime": 180 + }, + "operation": { + "airConOperationMode": "POWER_ON" + }, + "powerSave": { + "powerSaveEnabled": false + }, + "sleepTimer": { + "relativeStopTimer": "UNSET" + }, + "temperature": { + "currentTemperature": 25, + "targetTemperature": 19, + "unit": "C" + }, + "timer": { + "relativeStartTimer": "UNSET", + "relativeStopTimer": "UNSET", + "absoluteStartTimer": "SET", + "absoluteStopTimer": "UNSET", + "absoluteHourToStart": 13, + "absoluteMinuteToStart": 14 + } +} diff --git a/tests/components/lg_thinq/snapshots/test_climate.ambr b/tests/components/lg_thinq/snapshots/test_climate.ambr new file mode 100644 index 00000000000..e9470c3de03 --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_climate.ambr @@ -0,0 +1,86 @@ +# serializer version: 1 +# name: test_all_entities[climate.test_air_conditioner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'low', + 'high', + 'mid', + ]), + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 18, + 'preset_modes': list([ + 'air_clean', + ]), + 'target_temp_step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_air_conditioner', + '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': None, + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_climate_air_conditioner', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[climate.test_air_conditioner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 40, + 'current_temperature': 25, + 'fan_mode': 'mid', + 'fan_modes': list([ + 'low', + 'high', + 'mid', + ]), + 'friendly_name': 'Test air conditioner', + 'hvac_modes': list([ + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 18, + 'preset_mode': None, + 'preset_modes': list([ + 'air_clean', + ]), + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 19, + }), + 'context': , + 'entity_id': 'climate.test_air_conditioner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/lg_thinq/snapshots/test_event.ambr b/tests/components/lg_thinq/snapshots/test_event.ambr new file mode 100644 index 00000000000..025f4496aeb --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_event.ambr @@ -0,0 +1,55 @@ +# serializer version: 1 +# name: test_all_entities[event.test_air_conditioner_notification-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'water_is_full', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.test_air_conditioner_notification', + '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': 'Notification', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_notification', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[event.test_air_conditioner_notification-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'water_is_full', + ]), + 'friendly_name': 'Test air conditioner Notification', + }), + 'context': , + 'entity_id': 'event.test_air_conditioner_notification', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/lg_thinq/snapshots/test_number.ambr b/tests/components/lg_thinq/snapshots/test_number.ambr new file mode 100644 index 00000000000..68f01854501 --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_number.ambr @@ -0,0 +1,113 @@ +# serializer version: 1 +# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.test_air_conditioner_schedule_turn_off', + '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': 'Schedule turn-off', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_stop', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test air conditioner Schedule turn-off', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.test_air_conditioner_schedule_turn_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.test_air_conditioner_schedule_turn_on', + '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': 'Schedule turn-on', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_start', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test air conditioner Schedule turn-on', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.test_air_conditioner_schedule_turn_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..387df916eba --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -0,0 +1,205 @@ +# serializer version: 1 +# name: test_all_entities[sensor.test_air_conditioner_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Test air conditioner Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_pm1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM1', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm1', + 'friendly_name': 'Test air conditioner PM1', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_pm1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm10-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_pm10', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM10', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm10', + 'friendly_name': 'Test air conditioner PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_pm2_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'PM2.5', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pm25', + 'friendly_name': 'Test air conditioner PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- diff --git a/tests/components/lg_thinq/test_climate.py b/tests/components/lg_thinq/test_climate.py new file mode 100644 index 00000000000..24ed3ad230d --- /dev/null +++ b/tests/components/lg_thinq/test_climate.py @@ -0,0 +1,29 @@ +"""Tests for the LG Thinq climate platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy 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") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.CLIMATE]): + 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_config_flow.py b/tests/components/lg_thinq/test_config_flow.py index db0e2d29450..e7ee632810e 100644 --- a/tests/components/lg_thinq/test_config_flow.py +++ b/tests/components/lg_thinq/test_config_flow.py @@ -14,7 +14,10 @@ from tests.common import MockConfigEntry async def test_config_flow( - hass: HomeAssistant, mock_thinq_api: AsyncMock, mock_uuid: AsyncMock + hass: HomeAssistant, + mock_thinq_api: AsyncMock, + mock_uuid: AsyncMock, + mock_setup_entry: AsyncMock, ) -> None: """Test that an thinq entry is normally created.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lg_thinq/test_event.py b/tests/components/lg_thinq/test_event.py new file mode 100644 index 00000000000..bea758cb943 --- /dev/null +++ b/tests/components/lg_thinq/test_event.py @@ -0,0 +1,29 @@ +"""Tests for the LG Thinq event platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy 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") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.EVENT]): + 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_init.py b/tests/components/lg_thinq/test_init.py new file mode 100644 index 00000000000..7da7e79fec0 --- /dev/null +++ b/tests/components/lg_thinq/test_init.py @@ -0,0 +1,26 @@ +"""Tests for the LG ThinQ integration.""" + +from unittest.mock import AsyncMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/lg_thinq/test_number.py b/tests/components/lg_thinq/test_number.py new file mode 100644 index 00000000000..e578e4eba7a --- /dev/null +++ b/tests/components/lg_thinq/test_number.py @@ -0,0 +1,29 @@ +"""Tests for the LG Thinq number platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy 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") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.NUMBER]): + 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_sensor.py b/tests/components/lg_thinq/test_sensor.py new file mode 100644 index 00000000000..02b91b4771b --- /dev/null +++ b/tests/components/lg_thinq/test_sensor.py @@ -0,0 +1,29 @@ +"""Tests for the LG Thinq sensor platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy 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") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From a8974cc6769cb1b0be90a118bc1b5a9210517978 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Oct 2024 09:27:38 +0200 Subject: [PATCH 10/19] Revert "Add testing framework for LG ThinQ" (#127779) Revert "Add testing framework for LG ThinQ (#127237)" This reverts commit 62ecd50910cfb6d8696c8f6550b8b2f75542f301. --- tests/components/lg_thinq/__init__.py | 14 +- tests/components/lg_thinq/conftest.py | 34 +-- .../fixtures/air_conditioner/device.json | 9 - .../fixtures/air_conditioner/profile.json | 154 ------------- .../fixtures/air_conditioner/status.json | 43 ---- .../lg_thinq/snapshots/test_climate.ambr | 86 -------- .../lg_thinq/snapshots/test_event.ambr | 55 ----- .../lg_thinq/snapshots/test_number.ambr | 113 ---------- .../lg_thinq/snapshots/test_sensor.ambr | 205 ------------------ tests/components/lg_thinq/test_climate.py | 29 --- tests/components/lg_thinq/test_config_flow.py | 5 +- tests/components/lg_thinq/test_event.py | 29 --- tests/components/lg_thinq/test_init.py | 26 --- tests/components/lg_thinq/test_number.py | 29 --- tests/components/lg_thinq/test_sensor.py | 29 --- 15 files changed, 7 insertions(+), 853 deletions(-) delete mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/device.json delete mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/profile.json delete mode 100644 tests/components/lg_thinq/fixtures/air_conditioner/status.json delete mode 100644 tests/components/lg_thinq/snapshots/test_climate.ambr delete mode 100644 tests/components/lg_thinq/snapshots/test_event.ambr delete mode 100644 tests/components/lg_thinq/snapshots/test_number.ambr delete mode 100644 tests/components/lg_thinq/snapshots/test_sensor.ambr delete mode 100644 tests/components/lg_thinq/test_climate.py delete mode 100644 tests/components/lg_thinq/test_event.py delete mode 100644 tests/components/lg_thinq/test_init.py delete mode 100644 tests/components/lg_thinq/test_number.py delete mode 100644 tests/components/lg_thinq/test_sensor.py diff --git a/tests/components/lg_thinq/__init__.py b/tests/components/lg_thinq/__init__.py index a5ba55ab1c9..68ffb960f71 100644 --- a/tests/components/lg_thinq/__init__.py +++ b/tests/components/lg_thinq/__init__.py @@ -1,13 +1 @@ -"""Tests for the LG ThinQ integration.""" - -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() +"""Tests for the lgthinq integration.""" diff --git a/tests/components/lg_thinq/conftest.py b/tests/components/lg_thinq/conftest.py index 05cb3164137..cae2de61fa4 100644 --- a/tests/components/lg_thinq/conftest.py +++ b/tests/components/lg_thinq/conftest.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY from .const import MOCK_CONNECT_CLIENT_ID, MOCK_COUNTRY, MOCK_PAT, MOCK_UUID -from tests.common import MockConfigEntry, load_json_object_fixture +from tests.common import MockConfigEntry def mock_thinq_api_response( @@ -45,15 +45,6 @@ def mock_config_entry() -> MockConfigEntry: ) -@pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: - """Mock setting up a config entry.""" - with patch( - "homeassistant.components.lg_thinq.async_setup_entry", return_value=True - ) as mock_setup_entry: - yield mock_setup_entry - - @pytest.fixture def mock_uuid() -> Generator[AsyncMock]: """Mock a uuid.""" @@ -68,37 +59,22 @@ def mock_uuid() -> Generator[AsyncMock]: @pytest.fixture -def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]: +def mock_thinq_api() -> Generator[AsyncMock]: """Mock a thinq api.""" with ( - patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api, + patch("thinqconnect.ThinQApi", autospec=True) as mock_api, patch( "homeassistant.components.lg_thinq.config_flow.ThinQApi", new=mock_api, ), ): thinq_api = mock_api.return_value - thinq_api.async_get_device_list.return_value = [ - load_json_object_fixture("air_conditioner/device.json", DOMAIN) - ] - thinq_api.async_get_device_profile.return_value = load_json_object_fixture( - "air_conditioner/profile.json", DOMAIN - ) - thinq_api.async_get_device_status.return_value = load_json_object_fixture( - "air_conditioner/status.json", DOMAIN + thinq_api.async_get_device_list = AsyncMock( + return_value=mock_thinq_api_response(status=200, body={}) ) yield thinq_api -@pytest.fixture -def mock_thinq_mqtt_client() -> Generator[AsyncMock]: - """Mock a thinq api.""" - with patch( - "homeassistant.components.lg_thinq.mqtt.ThinQMQTTClient", autospec=True - ) as mock_api: - yield mock_api - - @pytest.fixture def mock_invalid_thinq_api(mock_thinq_api: AsyncMock) -> AsyncMock: """Mock an invalid thinq api.""" diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/device.json b/tests/components/lg_thinq/fixtures/air_conditioner/device.json deleted file mode 100644 index fb931c69929..00000000000 --- a/tests/components/lg_thinq/fixtures/air_conditioner/device.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "deviceId": "MW2-2E247F93-B570-46A6-B827-920E9E10F966", - "deviceInfo": { - "deviceType": "DEVICE_AIR_CONDITIONER", - "modelName": "PAC_910604_WW", - "alias": "Test air conditioner", - "reportable": true - } -} diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/profile.json b/tests/components/lg_thinq/fixtures/air_conditioner/profile.json deleted file mode 100644 index 0d45dc5c9f4..00000000000 --- a/tests/components/lg_thinq/fixtures/air_conditioner/profile.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "notification": { - "push": ["WATER_IS_FULL"] - }, - "property": { - "airConJobMode": { - "currentJobMode": { - "mode": ["r", "w"], - "type": "enum", - "value": { - "r": ["AIR_CLEAN", "COOL", "AIR_DRY"], - "w": ["AIR_CLEAN", "COOL", "AIR_DRY"] - } - } - }, - "airFlow": { - "windStrength": { - "mode": ["r", "w"], - "type": "enum", - "value": { - "r": ["LOW", "HIGH", "MID"], - "w": ["LOW", "HIGH", "MID"] - } - } - }, - "airQualitySensor": { - "PM1": { - "mode": ["r"], - "type": "number" - }, - "PM10": { - "mode": ["r"], - "type": "number" - }, - "PM2": { - "mode": ["r"], - "type": "number" - }, - "humidity": { - "mode": ["r"], - "type": "number" - }, - "monitoringEnabled": { - "mode": ["r", "w"], - "type": "enum", - "value": { - "r": ["ON_WORKING", "ALWAYS"], - "w": ["ON_WORKING", "ALWAYS"] - } - }, - "oder": { - "mode": ["r"], - "type": "number" - }, - "totalPollution": { - "mode": ["r"], - "type": "number" - } - }, - "operation": { - "airCleanOperationMode": { - "mode": ["w"], - "type": "enum", - "value": { - "w": ["START", "STOP"] - } - }, - "airConOperationMode": { - "mode": ["r", "w"], - "type": "enum", - "value": { - "r": ["POWER_ON", "POWER_OFF"], - "w": ["POWER_ON", "POWER_OFF"] - } - } - }, - "powerSave": { - "powerSaveEnabled": { - "mode": ["r", "w"], - "type": "boolean", - "value": { - "r": [false, true], - "w": [false, true] - } - } - }, - "temperature": { - "coolTargetTemperature": { - "mode": ["w"], - "type": "range", - "value": { - "w": { - "max": 30, - "min": 18, - "step": 1 - } - } - }, - "currentTemperature": { - "mode": ["r"], - "type": "number" - }, - "targetTemperature": { - "mode": ["r", "w"], - "type": "range", - "value": { - "r": { - "max": 30, - "min": 18, - "step": 1 - }, - "w": { - "max": 30, - "min": 18, - "step": 1 - } - } - }, - "unit": { - "mode": ["r"], - "type": "enum", - "value": { - "r": ["C", "F"] - } - } - }, - "timer": { - "relativeHourToStart": { - "mode": ["r", "w"], - "type": "number" - }, - "relativeHourToStop": { - "mode": ["r", "w"], - "type": "number" - }, - "relativeMinuteToStart": { - "mode": ["r", "w"], - "type": "number" - }, - "relativeMinuteToStop": { - "mode": ["r", "w"], - "type": "number" - }, - "absoluteHourToStart": { - "mode": ["r", "w"], - "type": "number" - }, - "absoluteMinuteToStart": { - "mode": ["r", "w"], - "type": "number" - } - } - } -} diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/status.json b/tests/components/lg_thinq/fixtures/air_conditioner/status.json deleted file mode 100644 index 90d15d1ae16..00000000000 --- a/tests/components/lg_thinq/fixtures/air_conditioner/status.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "airConJobMode": { - "currentJobMode": "COOL" - }, - "airFlow": { - "windStrength": "MID" - }, - "airQualitySensor": { - "PM1": 12, - "PM10": 7, - "PM2": 24, - "humidity": 40, - "monitoringEnabled": "ON_WORKING", - "totalPollution": 3, - "totalPollutionLevel": "GOOD" - }, - "filterInfo": { - "filterLifetime": 540, - "usedTime": 180 - }, - "operation": { - "airConOperationMode": "POWER_ON" - }, - "powerSave": { - "powerSaveEnabled": false - }, - "sleepTimer": { - "relativeStopTimer": "UNSET" - }, - "temperature": { - "currentTemperature": 25, - "targetTemperature": 19, - "unit": "C" - }, - "timer": { - "relativeStartTimer": "UNSET", - "relativeStopTimer": "UNSET", - "absoluteStartTimer": "SET", - "absoluteStopTimer": "UNSET", - "absoluteHourToStart": 13, - "absoluteMinuteToStart": 14 - } -} diff --git a/tests/components/lg_thinq/snapshots/test_climate.ambr b/tests/components/lg_thinq/snapshots/test_climate.ambr deleted file mode 100644 index e9470c3de03..00000000000 --- a/tests/components/lg_thinq/snapshots/test_climate.ambr +++ /dev/null @@ -1,86 +0,0 @@ -# serializer version: 1 -# name: test_all_entities[climate.test_air_conditioner-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'fan_modes': list([ - 'low', - 'high', - 'mid', - ]), - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 30, - 'min_temp': 18, - 'preset_modes': list([ - 'air_clean', - ]), - 'target_temp_step': 1, - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'climate', - 'entity_category': None, - 'entity_id': 'climate.test_air_conditioner', - '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': None, - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': , - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_climate_air_conditioner', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[climate.test_air_conditioner-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_humidity': 40, - 'current_temperature': 25, - 'fan_mode': 'mid', - 'fan_modes': list([ - 'low', - 'high', - 'mid', - ]), - 'friendly_name': 'Test air conditioner', - 'hvac_modes': list([ - , - , - , - ]), - 'max_temp': 30, - 'min_temp': 18, - 'preset_mode': None, - 'preset_modes': list([ - 'air_clean', - ]), - 'supported_features': , - 'target_temp_step': 1, - 'temperature': 19, - }), - 'context': , - 'entity_id': 'climate.test_air_conditioner', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'cool', - }) -# --- diff --git a/tests/components/lg_thinq/snapshots/test_event.ambr b/tests/components/lg_thinq/snapshots/test_event.ambr deleted file mode 100644 index 025f4496aeb..00000000000 --- a/tests/components/lg_thinq/snapshots/test_event.ambr +++ /dev/null @@ -1,55 +0,0 @@ -# serializer version: 1 -# name: test_all_entities[event.test_air_conditioner_notification-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'event_types': list([ - 'water_is_full', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'event', - 'entity_category': None, - 'entity_id': 'event.test_air_conditioner_notification', - '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': 'Notification', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_notification', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[event.test_air_conditioner_notification-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'event_type': None, - 'event_types': list([ - 'water_is_full', - ]), - 'friendly_name': 'Test air conditioner Notification', - }), - 'context': , - 'entity_id': 'event.test_air_conditioner_notification', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- diff --git a/tests/components/lg_thinq/snapshots/test_number.ambr b/tests/components/lg_thinq/snapshots/test_number.ambr deleted file mode 100644 index 68f01854501..00000000000 --- a/tests/components/lg_thinq/snapshots/test_number.ambr +++ /dev/null @@ -1,113 +0,0 @@ -# serializer version: 1 -# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': None, - 'entity_id': 'number.test_air_conditioner_schedule_turn_off', - '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': 'Schedule turn-off', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_stop', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test air conditioner Schedule turn-off', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.test_air_conditioner_schedule_turn_off', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': None, - 'entity_id': 'number.test_air_conditioner_schedule_turn_on', - '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': 'Schedule turn-on', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': , - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_start', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test air conditioner Schedule turn-on', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1.0, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.test_air_conditioner_schedule_turn_on', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr deleted file mode 100644 index 387df916eba..00000000000 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ /dev/null @@ -1,205 +0,0 @@ -# serializer version: 1 -# name: test_all_entities[sensor.test_air_conditioner_humidity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_air_conditioner_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_humidity', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_humidity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'friendly_name': 'Test air conditioner Humidity', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.test_air_conditioner_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_air_conditioner_pm1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'PM1', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1', - 'unit_of_measurement': 'µg/m³', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pm1', - 'friendly_name': 'Test air conditioner PM1', - 'state_class': , - 'unit_of_measurement': 'µg/m³', - }), - 'context': , - 'entity_id': 'sensor.test_air_conditioner_pm1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '12', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm10-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_air_conditioner_pm10', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'PM10', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10', - 'unit_of_measurement': 'µg/m³', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm10-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pm10', - 'friendly_name': 'Test air conditioner PM10', - 'state_class': , - 'unit_of_measurement': 'µg/m³', - }), - 'context': , - 'entity_id': 'sensor.test_air_conditioner_pm10', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm2_5-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_air_conditioner_pm2_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'PM2.5', - 'platform': 'lg_thinq', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2', - 'unit_of_measurement': 'µg/m³', - }) -# --- -# name: test_all_entities[sensor.test_air_conditioner_pm2_5-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pm25', - 'friendly_name': 'Test air conditioner PM2.5', - 'state_class': , - 'unit_of_measurement': 'µg/m³', - }), - 'context': , - 'entity_id': 'sensor.test_air_conditioner_pm2_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '24', - }) -# --- diff --git a/tests/components/lg_thinq/test_climate.py b/tests/components/lg_thinq/test_climate.py deleted file mode 100644 index 24ed3ad230d..00000000000 --- a/tests/components/lg_thinq/test_climate.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for the LG Thinq climate platform.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from syrupy 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") -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_thinq_api: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.CLIMATE]): - 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_config_flow.py b/tests/components/lg_thinq/test_config_flow.py index e7ee632810e..db0e2d29450 100644 --- a/tests/components/lg_thinq/test_config_flow.py +++ b/tests/components/lg_thinq/test_config_flow.py @@ -14,10 +14,7 @@ from tests.common import MockConfigEntry async def test_config_flow( - hass: HomeAssistant, - mock_thinq_api: AsyncMock, - mock_uuid: AsyncMock, - mock_setup_entry: AsyncMock, + hass: HomeAssistant, mock_thinq_api: AsyncMock, mock_uuid: AsyncMock ) -> None: """Test that an thinq entry is normally created.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lg_thinq/test_event.py b/tests/components/lg_thinq/test_event.py deleted file mode 100644 index bea758cb943..00000000000 --- a/tests/components/lg_thinq/test_event.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for the LG Thinq event platform.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from syrupy 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") -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_thinq_api: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.EVENT]): - 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_init.py b/tests/components/lg_thinq/test_init.py deleted file mode 100644 index 7da7e79fec0..00000000000 --- a/tests/components/lg_thinq/test_init.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Tests for the LG ThinQ integration.""" - -from unittest.mock import AsyncMock - -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def test_load_unload_entry( - hass: HomeAssistant, - mock_thinq_api: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test load and unload entry.""" - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.LOADED - - await hass.config_entries.async_remove(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/lg_thinq/test_number.py b/tests/components/lg_thinq/test_number.py deleted file mode 100644 index e578e4eba7a..00000000000 --- a/tests/components/lg_thinq/test_number.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for the LG Thinq number platform.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from syrupy 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") -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_thinq_api: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.NUMBER]): - 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_sensor.py b/tests/components/lg_thinq/test_sensor.py deleted file mode 100644 index 02b91b4771b..00000000000 --- a/tests/components/lg_thinq/test_sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for the LG Thinq sensor platform.""" - -from unittest.mock import AsyncMock, patch - -import pytest -from syrupy 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") -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - mock_thinq_api: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, -) -> None: - """Test all entities.""" - with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.SENSOR]): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 1a12a0c1d82d6f75e0d92319dfefad7e7dd78109 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 8 Oct 2024 19:02:05 +0900 Subject: [PATCH 11/19] Add more switches to LG Thinq integration (#127901) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/icons.json | 33 +++++ .../components/lg_thinq/strings.json | 33 +++++ homeassistant/components/lg_thinq/switch.py | 136 ++++++++++++++++-- 3 files changed, 189 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index ea6fe78d95e..4e681c286ac 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -1,8 +1,41 @@ { "entity": { "switch": { + "auto_mode": { + "default": "mdi:cogs" + }, + "express_mode": { + "default": "mdi:snowflake-variant" + }, + "hot_water_mode": { + "default": "mdi:list-status" + }, + "humidity_warm_mode": { + "default": "mdi:heat-wave" + }, + "hygiene_dry_mode": { + "default": "mdi:format-list-bulleted" + }, + "mood_lamp_state": { + "default": "mdi:lamp" + }, "operation_power": { "default": "mdi:power" + }, + "optimal_humidity": { + "default": "mdi:water-percent" + }, + "power_save_enabled": { + "default": "mdi:hydro-power" + }, + "rapid_freeze": { + "default": "mdi:snowflake" + }, + "sleep_mode": { + "default": "mdi:format-list-bulleted" + }, + "uv_nano": { + "default": "mdi:air-filter" } }, "binary_sensor": { diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 45b088cb3f4..e9afc8c091b 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -20,8 +20,41 @@ }, "entity": { "switch": { + "auto_mode": { + "name": "Auto mode" + }, + "express_mode": { + "name": "Ice plus" + }, + "hot_water_mode": { + "name": "Hot water" + }, + "humidity_warm_mode": { + "name": "Warm mist" + }, + "hygiene_dry_mode": { + "name": "Drying mode" + }, + "mood_lamp_state": { + "name": "Mood light" + }, "operation_power": { "name": "Power" + }, + "optimal_humidity": { + "name": "Ventilation" + }, + "power_save_enabled": { + "name": "Energy saving" + }, + "rapid_freeze": { + "name": "Quick freeze" + }, + "sleep_mode": { + "name": "Sleep mode" + }, + "uv_nano": { + "name": "UVnano" } }, "binary_sensor": { diff --git a/homeassistant/components/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py index ead274f5753..a23c1cc45f8 100644 --- a/homeassistant/components/lg_thinq/switch.py +++ b/homeassistant/components/lg_thinq/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any @@ -14,39 +15,118 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ThinqConfigEntry from .entity import ThinQEntity -DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[SwitchEntityDescription, ...]] = { + +@dataclass(frozen=True, kw_only=True) +class ThinQSwitchEntityDescription(SwitchEntityDescription): + """Describes ThinQ switch entity.""" + + on_key: str | None = None + off_key: str | None = None + + +DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.POWER_SAVE_ENABLED, + translation_key=ThinQProperty.POWER_SAVE_ENABLED, + on_key="true", + off_key="false", + ), + ), DeviceType.AIR_PURIFIER_FAN: ( - SwitchEntityDescription( + ThinQSwitchEntityDescription( key=ThinQProperty.AIR_FAN_OPERATION_MODE, translation_key="operation_power" ), + ThinQSwitchEntityDescription( + key=ThinQProperty.UV_NANO, + translation_key=ThinQProperty.UV_NANO, + on_key="on", + off_key="off", + entity_category=EntityCategory.CONFIG, + ), ), DeviceType.AIR_PURIFIER: ( - SwitchEntityDescription( + ThinQSwitchEntityDescription( key=ThinQProperty.AIR_PURIFIER_OPERATION_MODE, translation_key="operation_power", ), ), DeviceType.DEHUMIDIFIER: ( - SwitchEntityDescription( + ThinQSwitchEntityDescription( key=ThinQProperty.DEHUMIDIFIER_OPERATION_MODE, translation_key="operation_power", ), ), DeviceType.HUMIDIFIER: ( - SwitchEntityDescription( + ThinQSwitchEntityDescription( key=ThinQProperty.HUMIDIFIER_OPERATION_MODE, translation_key="operation_power", ), + ThinQSwitchEntityDescription( + key=ThinQProperty.WARM_MODE, + translation_key="humidity_warm_mode", + on_key="warm_on", + off_key="warm_off", + entity_category=EntityCategory.CONFIG, + ), + ThinQSwitchEntityDescription( + key=ThinQProperty.MOOD_LAMP_STATE, + translation_key=ThinQProperty.MOOD_LAMP_STATE, + on_key="on", + off_key="off", + entity_category=EntityCategory.CONFIG, + ), + ThinQSwitchEntityDescription( + key=ThinQProperty.AUTO_MODE, + translation_key=ThinQProperty.AUTO_MODE, + on_key="auto_on", + off_key="auto_off", + entity_category=EntityCategory.CONFIG, + ), + ThinQSwitchEntityDescription( + key=ThinQProperty.SLEEP_MODE, + translation_key=ThinQProperty.SLEEP_MODE, + on_key="sleep_on", + off_key="sleep_off", + entity_category=EntityCategory.CONFIG, + ), + ), + DeviceType.REFRIGERATOR: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.EXPRESS_MODE, + translation_key=ThinQProperty.EXPRESS_MODE, + on_key="true", + off_key="false", + ), + ThinQSwitchEntityDescription( + key=ThinQProperty.RAPID_FREEZE, + translation_key=ThinQProperty.RAPID_FREEZE, + on_key="true", + off_key="false", + entity_category=EntityCategory.CONFIG, + ), ), DeviceType.SYSTEM_BOILER: ( - SwitchEntityDescription( - key=ThinQProperty.BOILER_OPERATION_MODE, translation_key="operation_power" + ThinQSwitchEntityDescription( + key=ThinQProperty.HOT_WATER_MODE, + translation_key=ThinQProperty.HOT_WATER_MODE, + on_key="on", + off_key="off", + ), + ), + DeviceType.WINE_CELLAR: ( + ThinQSwitchEntityDescription( + key=ThinQProperty.OPTIMAL_HUMIDITY, + translation_key=ThinQProperty.OPTIMAL_HUMIDITY, + on_key="on", + off_key="off", ), ), } @@ -82,26 +162,56 @@ async def async_setup_entry( class ThinQSwitchEntity(ThinQEntity, SwitchEntity): """Represent a thinq switch platform.""" + entity_description: ThinQSwitchEntityDescription _attr_device_class = SwitchDeviceClass.SWITCH def _update_status(self) -> None: """Update status itself.""" super()._update_status() + if (key := self.entity_description.on_key) is not None: + self._attr_is_on = self.data.value == key + else: + self._attr_is_on = self.data.is_on + _LOGGER.debug( - "[%s:%s] update status: %s", + "[%s:%s] update status: %s -> %s", self.coordinator.device_name, self.property_id, self.data.is_on, + self.is_on, ) - self._attr_is_on = self.data.is_on async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - _LOGGER.debug("[%s] async_turn_on", self.name) - await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id)) + _LOGGER.debug( + "[%s:%s] async_turn_on id: %s", + self.coordinator.device_name, + self.name, + self.property_id, + ) + if (on_command := self.entity_description.on_key) is not None: + await self.async_call_api( + self.coordinator.api.post(self.property_id, on_command) + ) + else: + await self.async_call_api( + self.coordinator.api.async_turn_on(self.property_id) + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - _LOGGER.debug("[%s] async_turn_off", self.name) - await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id)) + _LOGGER.debug( + "[%s:%s] async_turn_off id: %s", + self.coordinator.device_name, + self.name, + self.property_id, + ) + if (off_command := self.entity_description.off_key) is not None: + await self.async_call_api( + self.coordinator.api.post(self.property_id, off_command) + ) + else: + await self.async_call_api( + self.coordinator.api.async_turn_off(self.property_id) + ) From 47312793f86cdf2af872dae65902ea2d80a0dfdd Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 8 Oct 2024 19:22:08 +0900 Subject: [PATCH 12/19] Add select platform to LG ThinQ integration (#125709) * Add select platform to LG ThinQ integration * Update file mode * Add select platform to LG ThinQ integration * Update strings.json * Update select.py * Update __init__.py * Move entities from select to switch * Dedup translations --------- Co-authored-by: jangwon.lee Co-authored-by: Joost Lekkerkerker --- homeassistant/components/lg_thinq/__init__.py | 1 + homeassistant/components/lg_thinq/icons.json | 41 ++++ homeassistant/components/lg_thinq/select.py | 202 ++++++++++++++++++ .../components/lg_thinq/strings.json | 158 +++++++++++++- 4 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/lg_thinq/select.py diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index 6ca74cda8c0..a8d3fe175ef 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -43,6 +43,7 @@ PLATFORMS = [ Platform.EVENT, Platform.FAN, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.VACUUM, diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 4e681c286ac..9a54ff73a73 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -131,6 +131,47 @@ "default": "mdi:bed-clock" } }, + "select": { + "wind_strength": { + "default": "mdi:wind-power-outline" + }, + "monitoring_enabled": { + "default": "mdi:monitor-eye" + }, + "current_job_mode": { + "default": "mdi:format-list-bulleted" + }, + "operation_mode": { + "default": "mdi:gesture-tap-button" + }, + "operation_mode_for_location": { + "default": "mdi:gesture-tap-button" + }, + "air_clean_operation_mode": { + "default": "mdi:air-filter" + }, + "cook_mode": { + "default": "mdi:chef-hat" + }, + "cook_mode_for_location": { + "default": "mdi:chef-hat" + }, + "light_brightness": { + "default": "mdi:list-status" + }, + "wind_angle": { + "default": "mdi:rotate-360" + }, + "display_light": { + "default": "mdi:brightness-6" + }, + "fresh_air_filter": { + "default": "mdi:air-filter" + }, + "hygiene_dry_mode": { + "default": "mdi:format-list-bulleted" + } + }, "sensor": { "error": { "default": "mdi:alert-circle-outline" diff --git a/homeassistant/components/lg_thinq/select.py b/homeassistant/components/lg_thinq/select.py new file mode 100644 index 00000000000..45f1b5cbfa4 --- /dev/null +++ b/homeassistant/components/lg_thinq/select.py @@ -0,0 +1,202 @@ +"""Support for select entities.""" + +from __future__ import annotations + +import logging + +from thinqconnect import DeviceType +from thinqconnect.devices.const import Property as ThinQProperty +from thinqconnect.integration import ActiveMode + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + +SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = { + ThinQProperty.MONITORING_ENABLED: SelectEntityDescription( + key=ThinQProperty.MONITORING_ENABLED, + translation_key=ThinQProperty.MONITORING_ENABLED, + ), + ThinQProperty.COOK_MODE: SelectEntityDescription( + key=ThinQProperty.COOK_MODE, + translation_key=ThinQProperty.COOK_MODE, + ), + ThinQProperty.DISPLAY_LIGHT: SelectEntityDescription( + key=ThinQProperty.DISPLAY_LIGHT, + translation_key=ThinQProperty.DISPLAY_LIGHT, + ), + ThinQProperty.CURRENT_JOB_MODE: SelectEntityDescription( + key=ThinQProperty.CURRENT_JOB_MODE, + translation_key=ThinQProperty.CURRENT_JOB_MODE, + ), +} +AIR_FLOW_SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = { + ThinQProperty.WIND_STRENGTH: SelectEntityDescription( + key=ThinQProperty.WIND_STRENGTH, + translation_key=ThinQProperty.WIND_STRENGTH, + ), + ThinQProperty.WIND_ANGLE: SelectEntityDescription( + key=ThinQProperty.WIND_ANGLE, + translation_key=ThinQProperty.WIND_ANGLE, + ), +} +OPERATION_SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = { + ThinQProperty.AIR_CLEAN_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.AIR_CLEAN_OPERATION_MODE, + translation_key="air_clean_operation_mode", + ), + ThinQProperty.DISH_WASHER_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.DISH_WASHER_OPERATION_MODE, + translation_key="operation_mode", + ), + ThinQProperty.DRYER_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.DRYER_OPERATION_MODE, + translation_key="operation_mode", + ), + ThinQProperty.HYGIENE_DRY_MODE: SelectEntityDescription( + key=ThinQProperty.HYGIENE_DRY_MODE, + translation_key=ThinQProperty.HYGIENE_DRY_MODE, + ), + ThinQProperty.LIGHT_BRIGHTNESS: SelectEntityDescription( + key=ThinQProperty.LIGHT_BRIGHTNESS, + translation_key=ThinQProperty.LIGHT_BRIGHTNESS, + ), + ThinQProperty.OVEN_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.OVEN_OPERATION_MODE, + translation_key="operation_mode", + ), + ThinQProperty.STYLER_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.STYLER_OPERATION_MODE, + translation_key="operation_mode", + ), + ThinQProperty.WASHER_OPERATION_MODE: SelectEntityDescription( + key=ThinQProperty.WASHER_OPERATION_MODE, + translation_key="operation_mode", + ), +} + +DEVICE_TYPE_SELECT_MAP: dict[DeviceType, tuple[SelectEntityDescription, ...]] = { + DeviceType.AIR_CONDITIONER: ( + SELECT_DESC[ThinQProperty.MONITORING_ENABLED], + OPERATION_SELECT_DESC[ThinQProperty.AIR_CLEAN_OPERATION_MODE], + ), + DeviceType.AIR_PURIFIER_FAN: ( + AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH], + AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_ANGLE], + SELECT_DESC[ThinQProperty.DISPLAY_LIGHT], + SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE], + ), + DeviceType.AIR_PURIFIER: ( + AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH], + SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE], + ), + DeviceType.DEHUMIDIFIER: (AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH],), + DeviceType.DISH_WASHER: ( + OPERATION_SELECT_DESC[ThinQProperty.DISH_WASHER_OPERATION_MODE], + ), + DeviceType.DRYER: (OPERATION_SELECT_DESC[ThinQProperty.DRYER_OPERATION_MODE],), + DeviceType.HUMIDIFIER: ( + AIR_FLOW_SELECT_DESC[ThinQProperty.WIND_STRENGTH], + SELECT_DESC[ThinQProperty.DISPLAY_LIGHT], + SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE], + OPERATION_SELECT_DESC[ThinQProperty.HYGIENE_DRY_MODE], + ), + DeviceType.OVEN: ( + SELECT_DESC[ThinQProperty.COOK_MODE], + OPERATION_SELECT_DESC[ThinQProperty.OVEN_OPERATION_MODE], + ), + DeviceType.STYLER: (OPERATION_SELECT_DESC[ThinQProperty.STYLER_OPERATION_MODE],), + DeviceType.WASHCOMBO_MAIN: ( + OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], + ), + DeviceType.WASHCOMBO_MINI: ( + OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], + ), + DeviceType.WASHER: (OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE],), + DeviceType.WASHTOWER_DRYER: ( + OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], + ), + DeviceType.WASHTOWER: ( + OPERATION_SELECT_DESC[ThinQProperty.DRYER_OPERATION_MODE], + OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], + ), + DeviceType.WASHTOWER_WASHER: ( + OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], + ), + DeviceType.WATER_HEATER: (SELECT_DESC[ThinQProperty.CURRENT_JOB_MODE],), + DeviceType.WINE_CELLAR: (OPERATION_SELECT_DESC[ThinQProperty.LIGHT_BRIGHTNESS],), +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up an entry for select platform.""" + entities: list[ThinQSelectEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + descriptions := DEVICE_TYPE_SELECT_MAP.get( + coordinator.api.device.device_type + ) + ) is not None: + for description in descriptions: + entities.extend( + ThinQSelectEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx( + description.key, ActiveMode.WRITABLE + ) + ) + + if entities: + async_add_entities(entities) + + +class ThinQSelectEntity(ThinQEntity, SelectEntity): + """Represent a thinq select platform.""" + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: SelectEntityDescription, + property_id: str, + ) -> None: + """Initialize a select entity.""" + super().__init__(coordinator, entity_description, property_id) + + self._attr_options = self.data.options if self.data.options is not None else [] + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + if self.data.value: + self._attr_current_option = str(self.data.value) + else: + self._attr_current_option = None + + _LOGGER.debug( + "[%s:%s] update status: %s -> %s, options:%s", + self.coordinator.device_name, + self.property_id, + self.data.value, + self.current_option, + self.options, + ) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug( + "[%s:%s] async_select_option: %s", + self.coordinator.device_name, + self.property_id, + option, + ) + await self.async_call_api(self.coordinator.api.post(self.property_id, option)) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index e9afc8c091b..c47c128fcfb 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -547,7 +547,7 @@ "select": "My space", "smart_humidity": "Smart", "spot": "Spiral spot mode", - "turbo": "Turbo", + "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", "vacation": "Vacation", "zigzag": "Zigzag" } @@ -560,13 +560,13 @@ "mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]", "normal": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::normal%]", "off": "[%key:common::state::off%]", - "turbo": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::turbo%]" + "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]" } }, "personalization_mode": { "name": "Personal mode", "state": { - "auto_inside": "Auto mode", + "auto_inside": "[%key:component::lg_thinq::entity::switch::auto_mode::name%]", "sleep": "Sleep mode", "baby": "Baby care mode", "sick_house": "New Home mode", @@ -579,7 +579,7 @@ } }, "cock_state": { - "name": "UVnano", + "name": "[%key:component::lg_thinq::entity::switch::uv_nano::name%]", "state": { "cleaning": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", "normal": "[%key:common::state::standby%]" @@ -624,6 +624,156 @@ "temperature_number": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::temperature_number%]" } } + }, + "select": { + "wind_strength": { + "name": "Speed", + "state": { + "slow": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::fan_mode::state::slow%]", + "low": "Low", + "mid": "Medium", + "high": "High", + "power": "Turbo", + "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", + "auto": "Auto", + "wind_1": "Step 1", + "wind_2": "Step 2", + "wind_3": "Step 3", + "wind_4": "Step 4", + "wind_5": "Step 5", + "wind_6": "Step 6", + "wind_7": "Step 7", + "wind_8": "Step 8", + "wind_9": "Step 9", + "wind_10": "Step 10" + } + }, + "monitoring_enabled": { + "name": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::name%]", + "state": { + "on_working": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::state::on_working%]", + "always": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::state::always%]" + } + }, + "current_job_mode": { + "name": "Operating mode", + "state": { + "air_clean": "Purifying", + "auto": "[%key:component::lg_thinq::entity::select::wind_strength::state::auto%]", + "baby_care": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::baby%]", + "circulator": "Booster", + "clean": "Single", + "direct_clean": "Direct mode", + "dual_clean": "Dual", + "fast": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", + "heat_pump": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::heat_pump%]", + "humidify": "Mist", + "humidify_and_air_clean": "Mist & purifying", + "humidity": "Humid", + "nature_clean": "Natural mode", + "pet_clean": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::pet%]", + "silent": "Silent", + "sleep": "Sleep", + "smart": "Smart mode", + "space_clean": "Diffusion mode", + "spot_clean": "Wide mode", + "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", + "up_feature": "Additional mode", + "vacation": "Vacation" + } + }, + "operation_mode": { + "name": "Operation", + "state": { + "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]", + "power_off": "Power off", + "preheating": "Preheating", + "start": "[%key:common::action::start%]", + "stop": "[%key:common::action::stop%]", + "wake_up": "Sleep mode off" + } + }, + "operation_mode_for_location": { + "name": "{location} operation", + "state": { + "cancel": "[%key:component::lg_thinq::entity::sensor::current_state::state::cancel%]", + "power_off": "[%key:component::lg_thinq::entity::select::operation_mode::state::power_off%]", + "preheating": "[%key:component::lg_thinq::entity::select::operation_mode::state::preheating%]", + "start": "[%key:common::action::start%]", + "stop": "[%key:common::action::stop%]", + "wake_up": "[%key:component::lg_thinq::entity::select::operation_mode::state::wake_up%]" + } + }, + "air_clean_operation_mode": { + "name": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::preset_mode::state::air_clean%]", + "state": { + "start": "[%key:common::action::start%]", + "stop": "[%key:common::action::stop%]" + } + }, + "cook_mode": { + "name": "Cook mode", + "state": { + "bake": "Bake", + "convection_bake": "Convection bake", + "convection_roast": "Convection roast", + "roast": "Roast", + "crisp_convection": "Crisp convection" + } + }, + "cook_mode_for_location": { + "name": "{location} cook mode", + "state": { + "bake": "[%key:component::lg_thinq::entity::select::cook_mode::state::bake%]", + "convection_bake": "[%key:component::lg_thinq::entity::select::cook_mode::state::convection_bake%]", + "convection_roast": "[%key:component::lg_thinq::entity::select::cook_mode::state::convection_roast%]", + "roast": "[%key:component::lg_thinq::entity::select::cook_mode::state::roast%]", + "crisp_convection": "[%key:component::lg_thinq::entity::select::cook_mode::state::crisp_convection%]" + } + }, + "light_brightness": { + "name": "Light" + }, + "wind_angle": { + "name": "Rotation", + "state": { + "off": "[%key:common::state::off%]", + "angle_45": "45°", + "angle_60": "60°", + "angle_90": "90°", + "angle_140": "140°" + } + }, + "display_light": { + "name": "Display brightness", + "state": { + "off": "[%key:common::state::off%]", + "level_1": "Brightness 1", + "level_2": "Brightness 2", + "level_3": "Brightness 3" + } + }, + "fresh_air_filter": { + "name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]", + "state": { + "off": "[%key:common::state::off%]", + "auto": "[%key:component::lg_thinq::entity::select::wind_strength::state::auto%]", + "power": "Power", + "replace": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::replace%]", + "smart_power": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]", + "smart_off": "[%key:common::state::off%]", + "smart_on": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]" + } + }, + "hygiene_dry_mode": { + "name": "[%key:component::lg_thinq::entity::switch::hygiene_dry_mode::name%]", + "state": { + "off": "[%key:common::state::off%]", + "fast": "Fast", + "silent": "Silent", + "normal": "Delicate" + } + } } } } From ec30080225f1a15d545e2cb1a63e0d00745bc412 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Mon, 14 Oct 2024 18:31:42 +0900 Subject: [PATCH 13/19] Add sensor platform to LG ThinQ integration (#125799) * Add sensor platform to LG ThinQ integration * Use ActiveMode.READABLE for COOKTOP * modify strings for using referenses * add device_class for wind_volume * use state's references * Dedup --------- Co-authored-by: jangwon.lee Co-authored-by: yunseon.park Co-authored-by: Joostlek --- homeassistant/components/lg_thinq/icons.json | 142 +++++++++- homeassistant/components/lg_thinq/sensor.py | 246 ++++++++++++++++-- .../components/lg_thinq/strings.json | 241 ++++++++++++++++- 3 files changed, 593 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 9a54ff73a73..ca385245799 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -173,8 +173,8 @@ } }, "sensor": { - "error": { - "default": "mdi:alert-circle-outline" + "odor_level": { + "default": "mdi:scent" }, "current_temperature": { "default": "mdi:thermometer" @@ -182,6 +182,9 @@ "temperature": { "default": "mdi:thermometer" }, + "total_pollution_level": { + "default": "mdi:air-filter" + }, "monitoring_enabled": { "default": "mdi:monitor-eye" }, @@ -197,6 +200,18 @@ "wind_volume_for_location": { "default": "mdi:wind-power-outline" }, + "brightness": { + "default": "mdi:tune-vertical-variant" + }, + "brightness_for_location": { + "default": "mdi:tune-vertical-variant" + }, + "duration": { + "default": "mdi:tune-vertical-variant" + }, + "duration_for_location": { + "default": "mdi:tune-vertical-variant" + }, "day_target_temperature": { "default": "mdi:thermometer" }, @@ -224,6 +239,9 @@ "fresh_air_filter": { "default": "mdi:air-filter" }, + "filter_lifetime": { + "default": "mdi:air-filter" + }, "used_time": { "default": "mdi:air-filter" }, @@ -236,6 +254,15 @@ "personalization_mode": { "default": "mdi:dots-circle" }, + "current_dish_washing_course": { + "default": "mdi:format-list-checks" + }, + "rinse_level": { + "default": "mdi:tune-vertical-variant" + }, + "softening_level": { + "default": "mdi:tune-vertical-variant" + }, "cock_state": { "default": "mdi:air-filter" }, @@ -250,6 +277,117 @@ }, "target_temperature_for_location": { "default": "mdi:thermometer" + }, + "elapsed_day_state": { + "default": "mdi:calendar-range-outline" + }, + "elapsed_day_total": { + "default": "mdi:calendar-range-outline" + }, + "recipe_name": { + "default": "mdi:information-box-outline" + }, + "wort_info": { + "default": "mdi:information-box-outline" + }, + "yeast_info": { + "default": "mdi:information-box-outline" + }, + "hop_oil_info": { + "default": "mdi:information-box-outline" + }, + "flavor_info": { + "default": "mdi:information-box-outline" + }, + "beer_remain": { + "default": "mdi:glass-mug-variant" + }, + "battery_level": { + "default": "mdi:battery-medium" + }, + "relative_to_start": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_start_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_start_wm": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_start_wm_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_stop": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_stop_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_stop_wm": { + "default": "mdi:clock-time-three-outline" + }, + "relative_to_stop_wm_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "sleep_timer_relative_to_stop": { + "default": "mdi:bed-clock" + }, + "sleep_timer_relative_to_stop_for_location": { + "default": "mdi:bed-clock" + }, + "absolute_to_start": { + "default": "mdi:clock-time-three-outline" + }, + "absolute_to_start_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "absolute_to_stop": { + "default": "mdi:clock-time-three-outline" + }, + "absolute_to_stop_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "remain": { + "default": "mdi:timer-sand" + }, + "remain_for_location": { + "default": "mdi:timer-sand" + }, + "running": { + "default": "mdi:timer-play-outline" + }, + "running_for_location": { + "default": "mdi:timer-play-outline" + }, + "total": { + "default": "mdi:timer-play-outline" + }, + "total_for_location": { + "default": "mdi:timer-play-outline" + }, + "target": { + "default": "mdi:clock-time-three-outline" + }, + "target_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "light_start": { + "default": "mdi:clock-time-three-outline" + }, + "light_start_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "light_end": { + "default": "mdi:clock-time-three-outline" + }, + "light_end_for_location": { + "default": "mdi:clock-time-three-outline" + }, + "power_level": { + "default": "mdi:radiator" + }, + "power_level_for_location": { + "default": "mdi:radiator" } } } diff --git a/homeassistant/components/lg_thinq/sensor.py b/homeassistant/components/lg_thinq/sensor.py index ca076bed729..79d785666ac 100644 --- a/homeassistant/components/lg_thinq/sensor.py +++ b/homeassistant/components/lg_thinq/sensor.py @@ -6,7 +6,7 @@ import logging from thinqconnect import DeviceType from thinqconnect.devices.const import Property as ThinQProperty -from thinqconnect.integration import ActiveMode, ThinQPropertyEx +from thinqconnect.integration import ActiveMode, ThinQPropertyEx, TimerProperty from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,6 +18,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,12 +27,6 @@ from . import ThinqConfigEntry from .coordinator import DeviceDataUpdateCoordinator from .entity import ThinQEntity -ERROR_DESC = SensorEntityDescription( - key=ThinQPropertyEx.ERROR, - device_class=SensorDeviceClass.ENUM, - translation_key=ThinQPropertyEx.ERROR, -) - AIR_QUALITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.PM1: SensorEntityDescription( key=ThinQProperty.PM1, @@ -64,11 +59,40 @@ AIR_QUALITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ), ThinQProperty.TEMPERATURE: SensorEntityDescription( key=ThinQProperty.TEMPERATURE, - device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.TEMPERATURE, ), + ThinQProperty.ODOR_LEVEL: SensorEntityDescription( + key=ThinQProperty.ODOR_LEVEL, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.ODOR_LEVEL, + ), + ThinQProperty.TOTAL_POLLUTION_LEVEL: SensorEntityDescription( + key=ThinQProperty.TOTAL_POLLUTION_LEVEL, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.TOTAL_POLLUTION_LEVEL, + ), +} +BATTERY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.BATTERY_PERCENT: SensorEntityDescription( + key=ThinQProperty.BATTERY_PERCENT, + translation_key=ThinQProperty.BATTERY_LEVEL, + ), +} +DISH_WASHING_COURSE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.CURRENT_DISH_WASHING_COURSE: SensorEntityDescription( + key=ThinQProperty.CURRENT_DISH_WASHING_COURSE, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.CURRENT_DISH_WASHING_COURSE, + ) +} +FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.FILTER_LIFETIME: SensorEntityDescription( + key=ThinQProperty.FILTER_LIFETIME, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=ThinQProperty.FILTER_LIFETIME, + ), } HUMIDITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.CURRENT_HUMIDITY: SensorEntityDescription( @@ -95,6 +119,65 @@ JOB_MODE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { translation_key=ThinQProperty.PERSONALIZATION_MODE, ), } +LIGHT_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.BRIGHTNESS: SensorEntityDescription( + key=ThinQProperty.BRIGHTNESS, + translation_key=ThinQProperty.BRIGHTNESS, + ), + ThinQProperty.DURATION: SensorEntityDescription( + key=ThinQProperty.DURATION, + native_unit_of_measurement=UnitOfTime.HOURS, + translation_key=ThinQProperty.DURATION, + ), +} +POWER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.POWER_LEVEL: SensorEntityDescription( + key=ThinQProperty.POWER_LEVEL, + translation_key=ThinQProperty.POWER_LEVEL, + ) +} +PREFERENCE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.RINSE_LEVEL: SensorEntityDescription( + key=ThinQProperty.RINSE_LEVEL, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.RINSE_LEVEL, + ), + ThinQProperty.SOFTENING_LEVEL: SensorEntityDescription( + key=ThinQProperty.SOFTENING_LEVEL, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.SOFTENING_LEVEL, + ), +} +RECIPE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.RECIPE_NAME: SensorEntityDescription( + key=ThinQProperty.RECIPE_NAME, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.RECIPE_NAME, + ), + ThinQProperty.WORT_INFO: SensorEntityDescription( + key=ThinQProperty.WORT_INFO, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.WORT_INFO, + ), + ThinQProperty.YEAST_INFO: SensorEntityDescription( + key=ThinQProperty.YEAST_INFO, + device_class=SensorDeviceClass.ENUM, + translation_key=ThinQProperty.YEAST_INFO, + ), + ThinQProperty.HOP_OIL_INFO: SensorEntityDescription( + key=ThinQProperty.HOP_OIL_INFO, + translation_key=ThinQProperty.HOP_OIL_INFO, + ), + ThinQProperty.FLAVOR_INFO: SensorEntityDescription( + key=ThinQProperty.FLAVOR_INFO, + translation_key=ThinQProperty.FLAVOR_INFO, + ), + ThinQProperty.BEER_REMAIN: SensorEntityDescription( + key=ThinQProperty.BEER_REMAIN, + native_unit_of_measurement=PERCENTAGE, + translation_key=ThinQProperty.BEER_REMAIN, + ), +} REFRIGERATION_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.FRESH_AIR_FILTER: SensorEntityDescription( key=ThinQProperty.FRESH_AIR_FILTER, @@ -132,20 +215,17 @@ RUN_STATE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { TEMPERATURE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.TARGET_TEMPERATURE, - device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key=ThinQProperty.TARGET_TEMPERATURE, ), ThinQProperty.DAY_TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.DAY_TARGET_TEMPERATURE, - device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.DAY_TARGET_TEMPERATURE, ), ThinQProperty.NIGHT_TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, - device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, @@ -162,16 +242,90 @@ TEMPERATURE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { translation_key=ThinQProperty.CURRENT_TEMPERATURE, ), } +WATER_FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + ThinQProperty.USED_TIME: SensorEntityDescription( + key=ThinQProperty.USED_TIME, + native_unit_of_measurement=UnitOfTime.MONTHS, + translation_key=ThinQProperty.USED_TIME, + ), +} WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.WATER_TYPE: SensorEntityDescription( key=ThinQProperty.WATER_TYPE, - device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.WATER_TYPE, ), } +TIMER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { + TimerProperty.RELATIVE_TO_START: SensorEntityDescription( + key=TimerProperty.RELATIVE_TO_START, + translation_key=TimerProperty.RELATIVE_TO_START, + ), + TimerProperty.RELATIVE_TO_START_WM: SensorEntityDescription( + key=TimerProperty.RELATIVE_TO_START, + translation_key=TimerProperty.RELATIVE_TO_START_WM, + ), + TimerProperty.RELATIVE_TO_STOP: SensorEntityDescription( + key=TimerProperty.RELATIVE_TO_STOP, + translation_key=TimerProperty.RELATIVE_TO_STOP, + ), + TimerProperty.RELATIVE_TO_STOP_WM: SensorEntityDescription( + key=TimerProperty.RELATIVE_TO_STOP, + translation_key=TimerProperty.RELATIVE_TO_STOP_WM, + ), + TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP: SensorEntityDescription( + key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP, + translation_key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP, + ), + TimerProperty.ABSOLUTE_TO_START: SensorEntityDescription( + key=TimerProperty.ABSOLUTE_TO_START, + translation_key=TimerProperty.ABSOLUTE_TO_START, + ), + TimerProperty.ABSOLUTE_TO_STOP: SensorEntityDescription( + key=TimerProperty.ABSOLUTE_TO_STOP, + translation_key=TimerProperty.ABSOLUTE_TO_STOP, + ), + TimerProperty.REMAIN: SensorEntityDescription( + key=TimerProperty.REMAIN, + translation_key=TimerProperty.REMAIN, + ), + TimerProperty.TARGET: SensorEntityDescription( + key=TimerProperty.TARGET, + translation_key=TimerProperty.TARGET, + ), + TimerProperty.RUNNING: SensorEntityDescription( + key=TimerProperty.RUNNING, + translation_key=TimerProperty.RUNNING, + ), + TimerProperty.TOTAL: SensorEntityDescription( + key=TimerProperty.TOTAL, + translation_key=TimerProperty.TOTAL, + ), + TimerProperty.LIGHT_START: SensorEntityDescription( + key=TimerProperty.LIGHT_START, + translation_key=TimerProperty.LIGHT_START, + ), + TimerProperty.LIGHT_END: SensorEntityDescription( + key=TimerProperty.LIGHT_END, + translation_key=TimerProperty.LIGHT_END, + ), + ThinQProperty.ELAPSED_DAY_STATE: SensorEntityDescription( + key=ThinQProperty.ELAPSED_DAY_STATE, + native_unit_of_measurement=UnitOfTime.DAYS, + translation_key=ThinQProperty.ELAPSED_DAY_STATE, + ), + ThinQProperty.ELAPSED_DAY_TOTAL: SensorEntityDescription( + key=ThinQProperty.ELAPSED_DAY_TOTAL, + native_unit_of_measurement=UnitOfTime.DAYS, + translation_key=ThinQProperty.ELAPSED_DAY_TOTAL, + ), +} + WASHER_SENSORS: tuple[SensorEntityDescription, ...] = ( - ERROR_DESC, RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM], + TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP_WM], + TIMER_SENSOR_DESC[TimerProperty.REMAIN], + TIMER_SENSOR_DESC[TimerProperty.TOTAL], ) DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = { DeviceType.AIR_CONDITIONER: ( @@ -179,6 +333,12 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], + FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_LIFETIME], + TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START], + TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP], + TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], ), DeviceType.AIR_PURIFIER_FAN: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], @@ -187,6 +347,9 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], + TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], ), DeviceType.AIR_PURIFIER: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], @@ -194,20 +357,42 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], JOB_MODE_SENSOR_DESC[ThinQProperty.PERSONALIZATION_MODE], ), - DeviceType.COOKTOP: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.COOKTOP: ( + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + POWER_SENSOR_DESC[ThinQProperty.POWER_LEVEL], + TIMER_SENSOR_DESC[TimerProperty.REMAIN], + ), DeviceType.DEHUMIDIFIER: ( JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], HUMIDITY_SENSOR_DESC[ThinQProperty.CURRENT_HUMIDITY], ), DeviceType.DISH_WASHER: ( - ERROR_DESC, + DISH_WASHING_COURSE_SENSOR_DESC[ThinQProperty.CURRENT_DISH_WASHING_COURSE], + PREFERENCE_SENSOR_DESC[ThinQProperty.RINSE_LEVEL], + PREFERENCE_SENSOR_DESC[ThinQProperty.SOFTENING_LEVEL], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM], + TIMER_SENSOR_DESC[TimerProperty.REMAIN], + TIMER_SENSOR_DESC[TimerProperty.TOTAL], ), DeviceType.DRYER: WASHER_SENSORS, - DeviceType.HOME_BREW: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.HOME_BREW: ( + RECIPE_SENSOR_DESC[ThinQProperty.RECIPE_NAME], + RECIPE_SENSOR_DESC[ThinQProperty.WORT_INFO], + RECIPE_SENSOR_DESC[ThinQProperty.YEAST_INFO], + RECIPE_SENSOR_DESC[ThinQProperty.HOP_OIL_INFO], + RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_INFO], + RECIPE_SENSOR_DESC[ThinQProperty.BEER_REMAIN], + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_STATE], + TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_TOTAL], + ), + DeviceType.HOOD: (TIMER_SENSOR_DESC[TimerProperty.REMAIN],), DeviceType.HUMIDIFIER: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], @@ -215,6 +400,10 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], + AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], + TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], + TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], + TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], ), DeviceType.KIMCHI_REFRIGERATOR: ( REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], @@ -223,28 +412,39 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = translation_key=ThinQProperty.TARGET_TEMPERATURE, ), ), - DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), + DeviceType.MICROWAVE_OVEN: ( + RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], + TIMER_SENSOR_DESC[TimerProperty.REMAIN], + ), DeviceType.OVEN: ( RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE], + TIMER_SENSOR_DESC[TimerProperty.REMAIN], + TIMER_SENSOR_DESC[TimerProperty.TARGET], ), DeviceType.PLANT_CULTIVATOR: ( + LIGHT_SENSOR_DESC[ThinQProperty.BRIGHTNESS], + LIGHT_SENSOR_DESC[ThinQProperty.DURATION], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], RUN_STATE_SENSOR_DESC[ThinQProperty.GROWTH_MODE], RUN_STATE_SENSOR_DESC[ThinQProperty.WIND_VOLUME], TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE], + TIMER_SENSOR_DESC[TimerProperty.LIGHT_START], + TIMER_SENSOR_DESC[TimerProperty.LIGHT_END], ), DeviceType.REFRIGERATOR: ( REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], + WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.USED_TIME], ), DeviceType.ROBOT_CLEANER: ( - ERROR_DESC, RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], + TIMER_SENSOR_DESC[TimerProperty.RUNNING], ), DeviceType.STICK_CLEANER: ( + BATTERY_SENSOR_DESC[ThinQProperty.BATTERY_PERCENT], JOB_MODE_SENSOR_DESC[ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], ), @@ -285,7 +485,12 @@ async def async_setup_entry( entities.extend( ThinQSensorEntity(coordinator, description, property_id) for property_id in coordinator.api.get_active_idx( - description.key, ActiveMode.READ_ONLY + description.key, + ( + ActiveMode.READABLE + if coordinator.api.device.device_type == DeviceType.COOKTOP + else ActiveMode.READ_ONLY + ), ) ) @@ -319,10 +524,11 @@ class ThinQSensorEntity(ThinQEntity, SensorEntity): self._attr_native_unit_of_measurement = data_unit _LOGGER.debug( - "[%s:%s] update status: %s, options:%s, unit:%s", + "[%s:%s] update status: %s -> %s, options:%s, unit:%s", self.coordinator.device_name, self.property_id, self.data.value, + self.native_value, self.options, self.native_unit_of_measurement, ) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index c47c128fcfb..0c8d4928cde 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -39,7 +39,7 @@ "name": "Mood light" }, "operation_power": { - "name": "Power" + "name": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]" }, "optimal_humidity": { "name": "Ventilation" @@ -101,8 +101,8 @@ "low": "Low", "mid": "Medium", "high": "High", - "power": "Power", - "auto": "Auto" + "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]" } }, "preset_mode": { @@ -274,12 +274,32 @@ } }, "sensor": { - "error": { - "name": "Error" + "odor_level": { + "name": "Odor", + "state": { + "invalid": "Invalid", + "weak": "Weak", + "normal": "Normal", + "strong": "Strong", + "very_strong": "Very strong" + } }, "current_temperature": { "name": "Current temperature" }, + "temperature": { + "name": "Temperature" + }, + "total_pollution_level": { + "name": "Overall air quality", + "state": { + "invalid": "Invalid", + "good": "Good", + "normal": "Moderate", + "bad": "Unhealthy", + "very_bad": "Poor" + } + }, "monitoring_enabled": { "name": "Air quality sensor", "state": { @@ -310,6 +330,18 @@ "wind_volume_for_location": { "name": "{location} wind speed" }, + "brightness": { + "name": "Lighting intensity" + }, + "brightness_for_location": { + "name": "{location} lighting intensity" + }, + "duration": { + "name": "Lighting duration" + }, + "duration_for_location": { + "name": "{location} lighting duration" + }, "day_target_temperature": { "name": "Day growth temperature" }, @@ -517,13 +549,16 @@ "state": { "off": "[%key:common::state::off%]", "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", - "power": "Power", + "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "replace": "Replace filter", "smart_power": "Smart safe storage", "smart_off": "[%key:common::state::off%]", "smart_on": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]" } }, + "filter_lifetime": { + "name": "Filter remaining" + }, "used_time": { "name": "Water filter used" }, @@ -535,7 +570,7 @@ "clothes_dry": "Laundry", "edge": "Edge cleaning", "heat_pump": "Heat pump", - "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", + "high": "Power", "intensive_dry": "Spot", "macro": "Custom mode", "mop": "Mop", @@ -556,7 +591,7 @@ "name": "Operating mode", "state": { "auto": "Low power", - "high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", + "high": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]", "normal": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::normal%]", "off": "[%key:common::state::off%]", @@ -578,10 +613,50 @@ "others": "Others" } }, + "current_dish_washing_course": { + "name": "Current cycle", + "state": { + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "heavy": "Intensive", + "delicate": "Delicate", + "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", + "normal": "Normal", + "rinse": "Rinse", + "refresh": "Refresh", + "express": "Express", + "machine_clean": "Machine clean", + "short_mode": "Short mode", + "download_cycle": "Download cycle", + "quick": "Quick", + "steam": "Steam care", + "spray": "Spray", + "eco": "Eco" + } + }, + "rinse_level": { + "name": "Rinse aid dispenser level", + "state": { + "rinselevel_0": "0", + "rinselevel_1": "1", + "rinselevel_2": "2", + "rinselevel_3": "3", + "rinselevel_4": "4" + } + }, + "softening_level": { + "name": "Softening level", + "state": { + "softeninglevel_0": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_0%]", + "softeninglevel_1": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_1%]", + "softeninglevel_2": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_2%]", + "softeninglevel_3": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_3%]", + "softeninglevel_4": "[%key:component::lg_thinq::entity::sensor::rinse_level::state::rinselevel_4%]" + } + }, "cock_state": { "name": "[%key:component::lg_thinq::entity::switch::uv_nano::name%]", "state": { - "cleaning": "[%key:component::lg_thinq::entity::sensor::current_state::state::cleaning%]", + "cleaning": "In progress", "normal": "[%key:common::state::standby%]" } }, @@ -623,6 +698,144 @@ "vegetable_fruit": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::vegetable_fruit%]", "temperature_number": "[%key:component::lg_thinq::entity::sensor::target_temperature::state::temperature_number%]" } + }, + "elapsed_day_state": { + "name": "Brewing period" + }, + "elapsed_day_total": { + "name": "Brewing duration" + }, + "recipe_name": { + "name": "Homebrew recipe", + "state": { + "ipa": "IPA", + "pale_ale": "Pale ale", + "stout": "Stout", + "wheat": "Wheat", + "pilsner": "Pilsner", + "red_ale": "Red ale", + "my_recipe": "My recipe" + } + }, + "wort_info": { + "name": "Wort", + "state": { + "hoppy": "Hoppy", + "deep_gold": "DeepGold", + "wheat": "Wheat", + "dark": "Dark" + } + }, + "yeast_info": { + "name": "Yeast", + "state": { + "american_ale": "American ale", + "english_ale": "English ale", + "lager": "Lager", + "weizen": "Weizen" + } + }, + "hop_oil_info": { + "name": "Hops" + }, + "flavor_info": { + "name": "Flavor" + }, + "beer_remain": { + "name": "Recipe progress" + }, + "battery_level": { + "name": "Battery", + "state": { + "high": "Full", + "mid": "Medium", + "low": "Low", + "warning": "Empty" + } + }, + "relative_to_start": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start::name%]" + }, + "relative_to_start_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_for_location::name%]" + }, + "relative_to_start_wm": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_wm::name%]" + }, + "relative_to_start_wm_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_wm_for_location::name%]" + }, + "relative_to_stop": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop::name%]" + }, + "relative_to_stop_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_for_location::name%]" + }, + "relative_to_stop_wm": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_wm::name%]" + }, + "relative_to_stop_wm_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_wm_for_location::name%]" + }, + "sleep_timer_relative_to_stop": { + "name": "[%key:component::lg_thinq::entity::number::sleep_timer_relative_hour_to_stop::name%]" + }, + "sleep_timer_relative_to_stop_for_location": { + "name": "[%key:component::lg_thinq::entity::number::sleep_timer_relative_hour_to_stop_for_location::name%]" + }, + "absolute_to_start": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start::name%]" + }, + "absolute_to_start_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_start_for_location::name%]" + }, + "absolute_to_stop": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop::name%]" + }, + "absolute_to_stop_for_location": { + "name": "[%key:component::lg_thinq::entity::number::relative_hour_to_stop_for_location::name%]" + }, + "remain": { + "name": "Remaining time" + }, + "remain_for_location": { + "name": "{location} remaining time" + }, + "running": { + "name": "Running time" + }, + "running_for_location": { + "name": "{location} running time" + }, + "total": { + "name": "Total time" + }, + "total_for_location": { + "name": "{location} total time" + }, + "target": { + "name": "Cook time" + }, + "target_for_location": { + "name": "{location} cook time" + }, + "light_start": { + "name": "Lights on time" + }, + "light_start_for_location": { + "name": "{location} lights on time" + }, + "light_end": { + "name": "Lights off time" + }, + "light_end_for_location": { + "name": "{location} lights off time" + }, + "power_level": { + "name": "Power level" + }, + "power_level_for_location": { + "name": "{location} power level" } }, "select": { @@ -635,7 +848,7 @@ "high": "High", "power": "Turbo", "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", - "auto": "Auto", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", "wind_1": "Step 1", "wind_2": "Step 2", "wind_3": "Step 3", @@ -659,7 +872,7 @@ "name": "Operating mode", "state": { "air_clean": "Purifying", - "auto": "[%key:component::lg_thinq::entity::select::wind_strength::state::auto%]", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", "baby_care": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::baby%]", "circulator": "Booster", "clean": "Single", @@ -757,8 +970,8 @@ "name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]", "state": { "off": "[%key:common::state::off%]", - "auto": "[%key:component::lg_thinq::entity::select::wind_strength::state::auto%]", - "power": "Power", + "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "replace": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::replace%]", "smart_power": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]", "smart_off": "[%key:common::state::off%]", @@ -771,7 +984,7 @@ "off": "[%key:common::state::off%]", "fast": "Fast", "silent": "Silent", - "normal": "Delicate" + "normal": "[%key:component::lg_thinq::entity::sensor::current_dish_washing_course::state::delicate%]" } } } From 0b6f55eab9392d9bbd4535a4a594f42c79dac57c Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Sun, 20 Oct 2024 22:12:40 +0900 Subject: [PATCH 14/19] Add warm_mode to switch platform (#128540) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/icons.json | 3 +++ homeassistant/components/lg_thinq/strings.json | 3 +++ homeassistant/components/lg_thinq/switch.py | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index ca385245799..3d3d8a7b78d 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -36,6 +36,9 @@ }, "uv_nano": { "default": "mdi:air-filter" + }, + "warm_mode": { + "default": "mdi:heat-wave" } }, "binary_sensor": { diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 0c8d4928cde..88d6aa20618 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -55,6 +55,9 @@ }, "uv_nano": { "name": "UVnano" + }, + "warm_mode": { + "name": "Heating" } }, "binary_sensor": { diff --git a/homeassistant/components/lg_thinq/switch.py b/homeassistant/components/lg_thinq/switch.py index a23c1cc45f8..905ef500db7 100644 --- a/homeassistant/components/lg_thinq/switch.py +++ b/homeassistant/components/lg_thinq/switch.py @@ -51,6 +51,13 @@ DEVICE_TYPE_SWITCH_MAP: dict[DeviceType, tuple[ThinQSwitchEntityDescription, ... off_key="off", entity_category=EntityCategory.CONFIG, ), + ThinQSwitchEntityDescription( + key=ThinQProperty.WARM_MODE, + translation_key=ThinQProperty.WARM_MODE, + on_key="warm_on", + off_key="warm_off", + entity_category=EntityCategory.CONFIG, + ), ), DeviceType.AIR_PURIFIER: ( ThinQSwitchEntityDescription( From ce582791db44b39dca3963254199102240998394 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 24 Oct 2024 20:09:53 +0900 Subject: [PATCH 15/19] Set climate_air_conditioner icons (#128617) Co-authored-by: Artem Draft Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/icons.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 3d3d8a7b78d..23ebecb81d2 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -76,6 +76,22 @@ "default": "mdi:air-filter" } }, + "climate": { + "climate_air_conditioner": { + "state_attributes": { + "fan_mode": { + "state": { + "slow": "mdi:fan-chevron-down", + "low": "mdi:fan-speed-1", + "mid": "mdi:fan-speed-2", + "high": "mdi:fan-speed-3", + "power": "mdi:fan-chevron-up", + "auto": "mdi:fan-auto" + } + } + } + } + }, "event": { "error": { "default": "mdi:alert-circle-outline" From 371a0d574db21b58eb53dabec8db09165ae53e03 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Fri, 25 Oct 2024 17:36:59 +0900 Subject: [PATCH 16/19] Remove endHour entity from LG Thinq (#129126) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/icons.json | 6 ------ homeassistant/components/lg_thinq/sensor.py | 5 ----- homeassistant/components/lg_thinq/strings.json | 6 ------ 3 files changed, 17 deletions(-) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 23ebecb81d2..87cf04e0c1a 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -396,12 +396,6 @@ "light_start_for_location": { "default": "mdi:clock-time-three-outline" }, - "light_end": { - "default": "mdi:clock-time-three-outline" - }, - "light_end_for_location": { - "default": "mdi:clock-time-three-outline" - }, "power_level": { "default": "mdi:radiator" }, diff --git a/homeassistant/components/lg_thinq/sensor.py b/homeassistant/components/lg_thinq/sensor.py index 79d785666ac..ea8d9c8dd69 100644 --- a/homeassistant/components/lg_thinq/sensor.py +++ b/homeassistant/components/lg_thinq/sensor.py @@ -304,10 +304,6 @@ TIMER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { key=TimerProperty.LIGHT_START, translation_key=TimerProperty.LIGHT_START, ), - TimerProperty.LIGHT_END: SensorEntityDescription( - key=TimerProperty.LIGHT_END, - translation_key=TimerProperty.LIGHT_END, - ), ThinQProperty.ELAPSED_DAY_STATE: SensorEntityDescription( key=ThinQProperty.ELAPSED_DAY_STATE, native_unit_of_measurement=UnitOfTime.DAYS, @@ -432,7 +428,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE], TIMER_SENSOR_DESC[TimerProperty.LIGHT_START], - TIMER_SENSOR_DESC[TimerProperty.LIGHT_END], ), DeviceType.REFRIGERATOR: ( REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 88d6aa20618..aac0b46ffd4 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -828,12 +828,6 @@ "light_start_for_location": { "name": "{location} lights on time" }, - "light_end": { - "name": "Lights off time" - }, - "light_end_for_location": { - "name": "{location} lights off time" - }, "power_level": { "name": "Power level" }, From c5c5778c0af15088b0aefe45760ca1cbe54e224c Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Fri, 25 Oct 2024 17:58:04 +0900 Subject: [PATCH 17/19] Fix refrigerator's freshAirFilter issue (#129124) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/select.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/lg_thinq/select.py b/homeassistant/components/lg_thinq/select.py index 45f1b5cbfa4..e555d616ca3 100644 --- a/homeassistant/components/lg_thinq/select.py +++ b/homeassistant/components/lg_thinq/select.py @@ -33,6 +33,10 @@ SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = { key=ThinQProperty.CURRENT_JOB_MODE, translation_key=ThinQProperty.CURRENT_JOB_MODE, ), + ThinQProperty.FRESH_AIR_FILTER: SelectEntityDescription( + key=ThinQProperty.FRESH_AIR_FILTER, + translation_key=ThinQProperty.FRESH_AIR_FILTER, + ), } AIR_FLOW_SELECT_DESC: dict[ThinQProperty, SelectEntityDescription] = { ThinQProperty.WIND_STRENGTH: SelectEntityDescription( @@ -109,6 +113,7 @@ DEVICE_TYPE_SELECT_MAP: dict[DeviceType, tuple[SelectEntityDescription, ...]] = SELECT_DESC[ThinQProperty.COOK_MODE], OPERATION_SELECT_DESC[ThinQProperty.OVEN_OPERATION_MODE], ), + DeviceType.REFRIGERATOR: (SELECT_DESC[ThinQProperty.FRESH_AIR_FILTER],), DeviceType.STYLER: (OPERATION_SELECT_DESC[ThinQProperty.STYLER_OPERATION_MODE],), DeviceType.WASHCOMBO_MAIN: ( OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], From 2cd2d8f9c44f73a714c7966b9afcd8528a590c3e Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Sat, 26 Oct 2024 01:42:08 +0900 Subject: [PATCH 18/19] Remove unnecessary code in event platform (#128552) Co-authored-by: jangwon.lee --- homeassistant/components/lg_thinq/event.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lg_thinq/event.py b/homeassistant/components/lg_thinq/event.py index dcbf55eb689..b963cba37cc 100644 --- a/homeassistant/components/lg_thinq/event.py +++ b/homeassistant/components/lg_thinq/event.py @@ -98,21 +98,16 @@ class ThinQEventEntity(ThinQEntity, EventEntity): """Update status itself.""" super()._update_status() - # Handle an event. - if (value := self.data.value) is None or value not in self.event_types: - return - - self._async_handle_update(value) - if self.entity_description.key == ThinQPropertyEx.NOTIFICATION: - self.coordinator.api.update_notification(None) - - _LOGGER.warning( - "[%s:%s] update status:%s, event_types:%s", + _LOGGER.debug( + "[%s:%s] update status: %s, event_types=%s", self.coordinator.device_name, self.property_id, self.data.value, self.event_types, ) + # Handle an event. + if (value := self.data.value) is not None and value in self.event_types: + self._async_handle_update(value) def _async_handle_update(self, value: str) -> None: """Handle the event.""" From 22ca247de7cd95e455943fb1ee406d40915eaed5 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Sat, 26 Oct 2024 06:46:59 +0900 Subject: [PATCH 19/19] Fix issues washtower can't get mqtt event. (#128556) Co-authored-by: Joost Lekkerkerker Co-authored-by: jangwon.lee Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/entity.py | 11 +++++------ homeassistant/components/lg_thinq/mqtt.py | 22 ++++++++++++++++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lg_thinq/entity.py b/homeassistant/components/lg_thinq/entity.py index 5cf3cd58837..f31b535dcaf 100644 --- a/homeassistant/components/lg_thinq/entity.py +++ b/homeassistant/components/lg_thinq/entity.py @@ -105,11 +105,10 @@ class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]): except ThinQAPIException as exc: if on_fail_method: on_fail_method() - raise ServiceValidationError( - exc.message, - translation_domain=DOMAIN, - translation_key=exc.code, + exc.message, translation_domain=DOMAIN, translation_key=exc.code ) from exc - finally: - await self.coordinator.async_request_refresh() + except ValueError as exc: + if on_fail_method: + on_fail_method() + raise ServiceValidationError(exc) from exc diff --git a/homeassistant/components/lg_thinq/mqtt.py b/homeassistant/components/lg_thinq/mqtt.py index 4d7abab73c8..30d1302e458 100644 --- a/homeassistant/components/lg_thinq/mqtt.py +++ b/homeassistant/components/lg_thinq/mqtt.py @@ -8,7 +8,13 @@ import json import logging from typing import Any -from thinqconnect import ThinQApi, ThinQAPIException, ThinQMQTTClient +from thinqconnect import ( + DeviceType, + ThinQApi, + ThinQAPIErrorCodes, + ThinQAPIException, + ThinQMQTTClient, +) from homeassistant.core import Event, HomeAssistant @@ -68,7 +74,10 @@ class ThinQMQTT: # and is not actually fail. return sum( isinstance(result, (TypeError, ValueError)) - or (isinstance(result, ThinQAPIException) and result.code != "1207") + or ( + isinstance(result, ThinQAPIException) + and result.code != ThinQAPIErrorCodes.ALREADY_SUBSCRIBED_PUSH + ) for result in results ) @@ -159,9 +168,12 @@ class ThinQMQTT: async def async_handle_device_event(self, message: dict) -> None: """Handle received mqtt message.""" _LOGGER.debug("async_handle_device_event: message=%s", message) - - device_id = message["deviceId"] - coordinator = self.coordinators.get(device_id) + unique_id = ( + f"{message["deviceId"]}_{list(message["report"].keys())[0]}" + if message["deviceType"] == DeviceType.WASHTOWER + else message["deviceId"] + ) + coordinator = self.coordinators.get(unique_id) if coordinator is None: _LOGGER.error("Failed to handle device event: No device") return