From 1d83f7dfa99dbcf35b9c0911fdb1ca3a286f5fa9 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Wed, 16 Aug 2023 19:54:35 -0600 Subject: [PATCH 01/13] Rework config flow for device discover and async msmart-ng --- custom_components/midea_ac/__init__.py | 25 +-- custom_components/midea_ac/climate.py | 6 +- custom_components/midea_ac/config_flow.py | 198 +++++++++++++----- custom_components/midea_ac/const.py | 2 +- custom_components/midea_ac/switch.py | 2 +- .../midea_ac/translations/en.json | 33 +-- 6 files changed, 181 insertions(+), 85 deletions(-) diff --git a/custom_components/midea_ac/__init__.py b/custom_components/midea_ac/__init__.py index 43313d1..58c437d 100644 --- a/custom_components/midea_ac/__init__.py +++ b/custom_components/midea_ac/__init__.py @@ -7,18 +7,13 @@ from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant from msmart.device import air_conditioning as ac -try: - # Try to import newer __version__ attribute - from msmart import __version__ as MSMART_VERISON -except ImportError: - # Fallback to older VERSION attribute - from msmart.device import VERSION as MSMART_VERISON +from msmart import __version__ as MSMART_VERISON # Local constants from .const import ( DOMAIN, - CONF_K1 + CONF_KEY ) from . import helpers @@ -51,16 +46,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Configure token and k1 as needed token = config.get(CONF_TOKEN) - k1 = config.get(CONF_K1) - if token and k1: - await hass.async_add_executor_job(device.authenticate, k1, token) + key = config.get(CONF_KEY) + if token and key: + success = await device.authenticate(token, key) + if not success: + _LOGGER.error("Failed to authenticate with device.") + return False hass.data[DOMAIN][id] = device # Query device capabilities if helpers.method_exists(device, "get_capabilities"): _LOGGER.info("Querying device capabilities.") - await hass.async_add_executor_job(device.get_capabilities) + await device.get_capabilities() # Create platform entries hass.async_create_task( @@ -86,7 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # Remove device from global data id = config.get(CONF_ID) - hass.data[DOMAIN].pop(id) + try: + hass.data[DOMAIN].pop(id) + except KeyError: + _LOGGER.warning("Failed remove device from global data.") await hass.config_entries.async_forward_entry_unload(config_entry, "climate") await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index 0e82461..d610b93 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -118,7 +118,7 @@ async def apply_changes(self) -> None: helpers.set_properties(self._device, ["fahrenheit", "fahrenheit_unit"], self.hass.config.units.temperature_unit == TEMP_FAHRENHEIT) - await self.hass.async_add_executor_job(self._device.apply) + await self._device.apply() self.async_write_ha_state() self._changed = False @@ -126,10 +126,10 @@ async def async_update(self) -> None: """Retrieve latest state from the appliance if no changes made, otherwise update the remote device state.""" if self._changed: - await self.hass.async_add_executor_job(self._device.apply) + await self._device.apply() self._changed = False elif not self._use_fan_only_workaround: - await self.hass.async_add_executor_job(self._device.refresh) + await self._device.refresh() async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py index b682cc3..18ed2b9 100644 --- a/custom_components/midea_ac/config_flow.py +++ b/custom_components/midea_ac/config_flow.py @@ -7,12 +7,14 @@ from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from msmart.device import air_conditioning as ac +from msmart.discover import Discover import voluptuous as vol +from typing import Any # Local constants from .const import ( DOMAIN, - CONF_K1, + CONF_KEY, CONF_PROMPT_TONE, CONF_TEMP_STEP, CONF_INCLUDE_OFF_AS_STATE, @@ -21,47 +23,129 @@ CONF_ADDITIONAL_OPERATION_MODES ) +_DEFAULT_OPTIONS = { + CONF_PROMPT_TONE: True, + CONF_TEMP_STEP: 1.0, + CONF_INCLUDE_OFF_AS_STATE: True, + CONF_USE_FAN_ONLY_WORKAROUND: False, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE: False, + CONF_ADDITIONAL_OPERATION_MODES: None, +} + class MideaConfigFlow(ConfigFlow, domain=DOMAIN): - async def async_step_user(self, user_input) -> FlowResult: + async def async_step_user(self, _) -> FlowResult: + + return self.async_show_menu( + step_id="user", + menu_options=["discover", "manual"], + ) + + async def async_step_discover( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + errors = {} + + if user_input is not None: + # If host was not provided, discover all devices + if not (host := user_input.get(CONF_HOST)): + return await self.async_step_pick_device() + + # Attempt to find specified device + device = await Discover.discover_single(host, auto_connect=False) + + if device is None: + errors["base"] = "no_devices_found" + else: + # Check if device has already been configured + await self.async_set_unique_id(device.id) + self._abort_if_unique_id_configured() + + # Finish connection + if await Discover.connect(device): + return await self._create_entry_from_device(device) + else: + # Indicate a connection could not be made + errors["base"] = "cannot_connect" + + data_schema = vol.Schema({ + vol.Optional(CONF_HOST, default=""): str + }) + + return self.async_show_form(step_id="discover", + data_schema=data_schema, errors=errors) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: errors = {} + if user_input is not None: - # Set the unique ID and abort if duplicate exists - id = user_input.get(CONF_ID) + # Find selected device + device = next(dev + for dev in self._discovered_devices + if dev.id == user_input[CONF_ID]) + + if device: + # Check if device has already been configured + await self.async_set_unique_id(device.id) + self._abort_if_unique_id_configured() + + # Finish connection + if await Discover.connect(device): + return await self._create_entry_from_device(device) + else: + # Indicate a connection could not be made + errors["base"] = "cannot_connect" + + # Create a set of already configured devices by ID + configured_devices = { + entry.unique_id for entry in self._async_current_entries() + } + + # Discover all devices + self._discovered_devices = await Discover.discover(auto_connect=False) + + # Create dict of device ID to friendly name + devices_name = { + device.id: ( + f"{device.name} - {device.id} ({device.ip})" + ) + for device in self._discovered_devices + if device.id not in configured_devices + } + + # Check if there is at least one device + if len(devices_name) == 0: + return self.async_abort(reason="no_devices_found") + + data_schema = vol.Schema({ + vol.Required(CONF_ID): vol.In(devices_name) + }) + + return self.async_show_form(step_id="pick_device", + data_schema=data_schema) + + async def async_step_manual(self, user_input) -> FlowResult: + errors = {} + + if user_input is not None: + # Get device ID from user input + id = int(user_input.get(CONF_ID)) + + # Check if device has already been configured await self.async_set_unique_id(id) self._abort_if_unique_id_configured() # Attempt a connection to see if config is valid - device = await self._test_connection(user_input) + device = await self._test_manual_connection(user_input) if device: - # Save the device into global data - self.hass.data.setdefault(DOMAIN, {}) - self.hass.data[DOMAIN][id] = device - - # Split user input config data and options - data = { - CONF_ID: id, - CONF_HOST: user_input.get(CONF_HOST), - CONF_PORT: user_input.get(CONF_PORT), - CONF_TOKEN: user_input.get(CONF_TOKEN), - CONF_K1: user_input.get(CONF_K1), - } - options = { - CONF_PROMPT_TONE: user_input.get(CONF_PROMPT_TONE), - CONF_TEMP_STEP: user_input.get(CONF_TEMP_STEP), - CONF_INCLUDE_OFF_AS_STATE: user_input.get(CONF_INCLUDE_OFF_AS_STATE), - CONF_USE_FAN_ONLY_WORKAROUND: user_input.get(CONF_USE_FAN_ONLY_WORKAROUND), - CONF_KEEP_LAST_KNOWN_ONLINE_STATE: user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE), - CONF_ADDITIONAL_OPERATION_MODES: user_input.get(CONF_ADDITIONAL_OPERATION_MODES), - } - - # Create a config entry with the config data and options - return self.async_create_entry(title=f"{DOMAIN} {id}", data=data, options=options) - else: - # Indicate a connection could not be made - errors["base"] = "cannot_connect" + return await self._create_entry_from_device(device) + + # Indicate a connection could not be made + errors["base"] = "cannot_connect" user_input = user_input or {} @@ -74,44 +158,50 @@ async def async_step_user(self, user_input) -> FlowResult: default=user_input.get(CONF_PORT, 6444)): cv.port, vol.Optional(CONF_TOKEN, description={"suggested_value": user_input.get(CONF_TOKEN, "")}): cv.string, - vol.Optional(CONF_K1, - description={"suggested_value": user_input.get(CONF_K1, "")}): cv.string, - vol.Optional(CONF_PROMPT_TONE, - default=user_input.get(CONF_PROMPT_TONE, True)): cv.boolean, - vol.Optional(CONF_TEMP_STEP, - default=user_input.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), - vol.Optional(CONF_INCLUDE_OFF_AS_STATE, - default=user_input.get(CONF_INCLUDE_OFF_AS_STATE, True)): cv.boolean, - vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, - default=user_input.get(CONF_USE_FAN_ONLY_WORKAROUND, False)): cv.boolean, - vol.Optional(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, - default=user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, False)): cv.boolean, - vol.Optional(CONF_ADDITIONAL_OPERATION_MODES, - description={"suggested_value": user_input.get(CONF_ADDITIONAL_OPERATION_MODES, None)}): cv.string, + vol.Optional(CONF_KEY, + description={"suggested_value": user_input.get(CONF_KEY, "")}): cv.string }) - return self.async_show_form(step_id="user", data_schema=data_schema, errors=errors) + return self.async_show_form(step_id="manual", + data_schema=data_schema, errors=errors) - async def _test_connection(self, config) -> ac | None: + async def _test_manual_connection(self, config) -> ac | None: # Construct the device id = config.get(CONF_ID) host = config.get(CONF_HOST) port = config.get(CONF_PORT) device = ac(host, int(id), port) - # Configure token and k1 as needed + # Configure token and key as needed token = config.get(CONF_TOKEN) - k1 = config.get(CONF_K1) - if token and k1: - success = await self.hass.async_add_executor_job(device.authenticate, k1, token) + key = config.get(CONF_KEY) + if token and key: + success = await device.authenticate(token, key) else: - await self.hass.async_add_executor_job(device.refresh) + await device.refresh() success = device.online return device if success else None - @staticmethod - @callback + async def _create_entry_from_device(self, device): + # Save the device into global data + self.hass.data.setdefault(DOMAIN, {}) + self.hass.data[DOMAIN][device.id] = device + + # Populate config data + data = { + CONF_ID: device.id, + CONF_HOST: device.ip, + CONF_PORT: device.port, + CONF_TOKEN: device.token, + CONF_KEY: device.key, + } + + # Create a config entry with the config data and default options + return self.async_create_entry(title=f"{DOMAIN} {device.id}", data=data, options=_DEFAULT_OPTIONS) + + @ staticmethod + @ callback def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: return MideaOptionsFlow(config_entry) diff --git a/custom_components/midea_ac/const.py b/custom_components/midea_ac/const.py index d2f2de1..2033469 100644 --- a/custom_components/midea_ac/const.py +++ b/custom_components/midea_ac/const.py @@ -1,6 +1,6 @@ DOMAIN = "midea_ac" -CONF_K1 = "k1" +CONF_KEY = "k1" CONF_PROMPT_TONE = "prompt_tone" CONF_TEMP_STEP = "temp_step" CONF_INCLUDE_OFF_AS_STATE = "include_off_as_state" diff --git a/custom_components/midea_ac/switch.py b/custom_components/midea_ac/switch.py index 356170b..503a5f4 100644 --- a/custom_components/midea_ac/switch.py +++ b/custom_components/midea_ac/switch.py @@ -46,7 +46,7 @@ def __init__(self, device): self._on = False async def _toggle_display(self) -> None: - await self.hass.async_add_executor_job(self._device.toggle_display) + await self._device.toggle_display() self.async_write_ha_state() self._on = not self._on diff --git a/custom_components/midea_ac/translations/en.json b/custom_components/midea_ac/translations/en.json index 0486f1a..43cfa67 100644 --- a/custom_components/midea_ac/translations/en.json +++ b/custom_components/midea_ac/translations/en.json @@ -2,40 +2,45 @@ "config": { "step": { "user": { - "title": "Configure Midea Smart AC Device", + "description": "Select how to add a device.", + "menu_options": { + "discover": "Discover device", + "manual": "Manually configure" + } + }, + "discover": { + "data": { + "host": "Host" + }, + "description": "Leave the host blank to discover device(s) on the network." + }, + "manual": { "description": "Enter information for your device.", "data": { "id": "ID", "host": "Host", "port": "Port", "token": "Token", - "k1": "K1", - "prompt_tone": "Prompt Tone", - "temp_step": "Temperature Step", - "include_off_as_state": "Include \"Off\" State", - "use_fan_only_workaround": "Use Fan-only Workaround", - "keep_last_known_online_state": "Keep Last Known State", - "additional_operation_modes": "Additional Operation Modes" + "k1": "Key" }, "data_description": { "token": "Token for V3 devices", - "k1": "K1 for V3 devices", - "prompt_tone": "Enable the beep when sending commands", - "temp_step": "Step size for temperature set point" + "k1": "Key for V3 devices" } } }, "abort":{ - "already_configured": "The device ID has already been configured." + "already_configured": "The device ID has already been configured.", + "no_devices_found": "No device(s) found on the network." }, "error":{ - "cannot_connect":"A connection could not be made with these settings." + "cannot_connect":"A connection could not be made with these settings.", + "no_devices_found": "No device(s) found on the network." } }, "options": { "step": { "init": { - "title": "Options for Midea Smart AC Device", "data": { "prompt_tone": "Prompt Tone", "temp_step": "Temperature Step", From e7c0b6b352404febda0d0abeae86ffed25006b24 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Wed, 16 Aug 2023 19:55:42 -0600 Subject: [PATCH 02/13] Apply isort --- custom_components/midea_ac/__init__.py | 10 +++---- custom_components/midea_ac/binary_sensor.py | 8 +++--- custom_components/midea_ac/climate.py | 29 +++++++++++---------- custom_components/midea_ac/config_flow.py | 21 ++++++--------- custom_components/midea_ac/sensor.py | 5 ++-- custom_components/midea_ac/switch.py | 7 ++--- 6 files changed, 38 insertions(+), 42 deletions(-) diff --git a/custom_components/midea_ac/__init__.py b/custom_components/midea_ac/__init__.py index 58c437d..7981e07 100644 --- a/custom_components/midea_ac/__init__.py +++ b/custom_components/midea_ac/__init__.py @@ -6,16 +6,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant -from msmart.device import air_conditioning as ac from msmart import __version__ as MSMART_VERISON +from msmart.device import air_conditioning as ac - -# Local constants -from .const import ( - DOMAIN, - CONF_KEY -) from . import helpers +# Local constants +from .const import CONF_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/midea_ac/binary_sensor.py b/custom_components/midea_ac/binary_sensor.py index e8928b0..b8896f1 100644 --- a/custom_components/midea_ac/binary_sensor.py +++ b/custom_components/midea_ac/binary_sensor.py @@ -3,17 +3,19 @@ import logging +from homeassistant.components.binary_sensor import (BinarySensorDeviceClass, + BinarySensorEntity) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.components.binary_sensor import BinarySensorDeviceClass, BinarySensorEntity +from homeassistant.const import (CONF_ID, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from . import helpers # Local constants from .const import DOMAIN -from . import helpers _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index d610b93..1d09463 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -12,29 +12,30 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_ID +from homeassistant.const import (ATTR_TEMPERATURE, CONF_ID, TEMP_CELSIUS, + TEMP_FAHRENHEIT) + try: from homeassistant.components.climate import ClimateEntity except ImportError: from homeassistant.components.climate import ClimateDevice as ClimateEntity -from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_PRESET_MODE, PRESET_NONE, PRESET_ECO, PRESET_BOOST, PRESET_AWAY, PRESET_SLEEP) + +from homeassistant.components.climate.const import (PRESET_AWAY, PRESET_BOOST, + PRESET_ECO, PRESET_NONE, + PRESET_SLEEP, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from msmart.device import air_conditioning as ac -# Local constants -from .const import ( - DOMAIN, - CONF_PROMPT_TONE, - CONF_TEMP_STEP, - CONF_INCLUDE_OFF_AS_STATE, - CONF_USE_FAN_ONLY_WORKAROUND, - CONF_KEEP_LAST_KNOWN_ONLINE_STATE, - CONF_ADDITIONAL_OPERATION_MODES -) from . import helpers +# Local constants +from .const import (CONF_ADDITIONAL_OPERATION_MODES, CONF_INCLUDE_OFF_AS_STATE, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE, CONF_PROMPT_TONE, + CONF_TEMP_STEP, CONF_USE_FAN_ONLY_WORKAROUND, DOMAIN) _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py index 18ed2b9..6e4fb27 100644 --- a/custom_components/midea_ac/config_flow.py +++ b/custom_components/midea_ac/config_flow.py @@ -1,27 +1,22 @@ """Config flow for Midea Smart AC.""" from __future__ import annotations +from typing import Any + +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv from msmart.device import air_conditioning as ac from msmart.discover import Discover -import voluptuous as vol -from typing import Any # Local constants -from .const import ( - DOMAIN, - CONF_KEY, - CONF_PROMPT_TONE, - CONF_TEMP_STEP, - CONF_INCLUDE_OFF_AS_STATE, - CONF_USE_FAN_ONLY_WORKAROUND, - CONF_KEEP_LAST_KNOWN_ONLINE_STATE, - CONF_ADDITIONAL_OPERATION_MODES -) +from .const import (CONF_ADDITIONAL_OPERATION_MODES, CONF_INCLUDE_OFF_AS_STATE, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE, CONF_KEY, + CONF_PROMPT_TONE, CONF_TEMP_STEP, + CONF_USE_FAN_ONLY_WORKAROUND, DOMAIN) _DEFAULT_OPTIONS = { CONF_PROMPT_TONE: True, diff --git a/custom_components/midea_ac/sensor.py b/custom_components/midea_ac/sensor.py index 926f315..947a988 100644 --- a/custom_components/midea_ac/sensor.py +++ b/custom_components/midea_ac/sensor.py @@ -3,9 +3,10 @@ import logging +from homeassistant.components.sensor import (RestoreSensor, SensorDeviceClass, + SensorStateClass) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, CONF_ID -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass, RestoreSensor +from homeassistant.const import CONF_ID, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/custom_components/midea_ac/switch.py b/custom_components/midea_ac/switch.py index 503a5f4..04f18f2 100644 --- a/custom_components/midea_ac/switch.py +++ b/custom_components/midea_ac/switch.py @@ -3,16 +3,17 @@ import logging -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, EntityCategory from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import (CONF_ID, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, EntityCategory) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from . import helpers # Local constants from .const import DOMAIN -from . import helpers _LOGGER = logging.getLogger(__name__) From 1e34153326e9754e61dc3448cb9aef9bec385c9d Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Wed, 16 Aug 2023 20:10:30 -0600 Subject: [PATCH 03/13] Tweak language for already configured error --- custom_components/midea_ac/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/midea_ac/translations/en.json b/custom_components/midea_ac/translations/en.json index 43cfa67..ce930ad 100644 --- a/custom_components/midea_ac/translations/en.json +++ b/custom_components/midea_ac/translations/en.json @@ -30,7 +30,7 @@ } }, "abort":{ - "already_configured": "The device ID has already been configured.", + "already_configured": "The device has already been configured.", "no_devices_found": "No device(s) found on the network." }, "error":{ From 58ee28aef3495592696a3d5dc50b884fc01b215d Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Wed, 16 Aug 2023 20:23:32 -0600 Subject: [PATCH 04/13] Reduce timeout for discovery to make the UX snappier --- custom_components/midea_ac/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py index 6e4fb27..f8ba03f 100644 --- a/custom_components/midea_ac/config_flow.py +++ b/custom_components/midea_ac/config_flow.py @@ -48,7 +48,7 @@ async def async_step_discover( return await self.async_step_pick_device() # Attempt to find specified device - device = await Discover.discover_single(host, auto_connect=False) + device = await Discover.discover_single(host, auto_connect=False, timeout=2) if device is None: errors["base"] = "no_devices_found" @@ -100,7 +100,7 @@ async def async_step_pick_device( } # Discover all devices - self._discovered_devices = await Discover.discover(auto_connect=False) + self._discovered_devices = await Discover.discover(auto_connect=False, timeout=2) # Create dict of device ID to friendly name devices_name = { From 478c301cd661eea7b79891c52628d7254b5d9c03 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Thu, 17 Aug 2023 19:02:50 -0600 Subject: [PATCH 05/13] Update class names and enum names for msmart-ng changes Use dictionaries to map between Midea modes and HA modes where able. Use new helper methods to convert between enum values and string values when necessary --- custom_components/midea_ac/__init__.py | 4 +- custom_components/midea_ac/climate.py | 149 +++++++++++++--------- custom_components/midea_ac/config_flow.py | 4 +- 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/custom_components/midea_ac/__init__.py b/custom_components/midea_ac/__init__.py index 7981e07..de17624 100644 --- a/custom_components/midea_ac/__init__.py +++ b/custom_components/midea_ac/__init__.py @@ -7,7 +7,7 @@ from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant from msmart import __version__ as MSMART_VERISON -from msmart.device import air_conditioning as ac +from msmart.device import AirConditioner as AC from . import helpers # Local constants @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b id = config.get(CONF_ID) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - device = ac(host, int(id), port) + device = AC(ip=host, port=port, device_id=int(id)) # Configure token and k1 as needed token = config.get(CONF_TOKEN) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index 1d09463..d7181d8 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -1,35 +1,24 @@ -""" -A climate platform that adds support for Midea air conditioning units. - -For more details about this platform, please refer to the documentation -https://github.com/mac-zhou/midea-ac-py - -This is still early work in progress -""" +"""Climate platform from Midea AC devices.""" from __future__ import annotations import datetime import logging -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import (ATTR_TEMPERATURE, CONF_ID, TEMP_CELSIUS, - TEMP_FAHRENHEIT) - -try: - from homeassistant.components.climate import ClimateEntity -except ImportError: - from homeassistant.components.climate import ClimateDevice as ClimateEntity - +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import (PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, PRESET_SLEEP, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE) + SUPPORT_TARGET_TEMPERATURE, + HVACMode) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import (ATTR_TEMPERATURE, CONF_ID, TEMP_CELSIUS, + TEMP_FAHRENHEIT) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from msmart.device import air_conditioning as ac +from msmart.device import AirConditioner as AC from . import helpers # Local constants @@ -42,6 +31,23 @@ # Override default scan interval? SCAN_INTERVAL = datetime.timedelta(seconds=15) +# Dictionaries to convert from Midea mode to HA mode +_OPERATIONAL_MODE_TO_HVAC_MODE: dict[AC.OperationalMode, HVACMode] = { + AC.OperationalMode.AUTO: HVACMode.AUTO, + AC.OperationalMode.COOL: HVACMode.COOL, + AC.OperationalMode.DRY: HVACMode.DRY, + AC.OperationalMode.HEAT: HVACMode.HEAT, + AC.OperationalMode.FAN_ONLY: HVACMode.FAN_ONLY, +} + +_HVAC_MODE_TO_OPERATIONAL_MODE: dict[HVACMode, AC.OperationalMode] = { + HVACMode.COOL: AC.OperationalMode.COOL, + HVACMode.HEAT: AC.OperationalMode.HEAT, + HVACMode.FAN_ONLY: AC.OperationalMode.FAN_ONLY, + HVACMode.DRY: AC.OperationalMode.DRY, + HVACMode.AUTO: AC.OperationalMode.AUTO, +} + async def async_setup_entry( hass: HomeAssistant, @@ -66,7 +72,7 @@ async def async_setup_entry( class MideaClimateACDevice(ClimateEntity): - """Representation of a Midea climate AC device.""" + """Climate entity for Midea AC device.""" def __init__(self, hass, device, options: dict): """Initialize the climate device.""" @@ -84,11 +90,17 @@ def __init__(self, hass, device, options: dict): self._use_fan_only_workaround = options.get( CONF_USE_FAN_ONLY_WORKAROUND) - # Attempt to load supported operation modes - self._operation_list = getattr( - self._device, "supported_operation_modes", ac.operational_mode_enum.list()) + # Fetch supported operational modes + supported_op_modes = getattr( + self._device, "supported_operation_modes", AC.OperationalMode.list()) + + # Convert from Midea operational modes to HA HVAC mode + self._operation_list = [_OPERATIONAL_MODE_TO_HVAC_MODE[m] + for m in supported_op_modes] + + # Include off mode if requested if self._include_off_as_state: - self._operation_list.append("off") + self._operation_list.append(HVACMode.OFF) # Append additional operation modes as needed additional_modes = options.get(CONF_ADDITIONAL_OPERATION_MODES) or "" @@ -97,11 +109,21 @@ def __init__(self, hass, device, options: dict): _LOGGER.info(f"Adding additional mode '{mode}'.") self._operation_list.append(mode) - self._fan_list = ac.fan_speed_enum.list() + # Convert Midea fan speeds to strings + self._fan_list = [m.name.capitalize() for m in AC.FanSpeed.list()] + + # Fetch supported swing modes + supported_swing_modes = getattr( + self._device, "supported_swing_modes", AC.SwingMode.list()) - # Attempt to load supported swing modes - self._swing_list = getattr( - self._device, "supported_swing_modes", ac.swing_mode_enum.list()) + # Convert Midea swing modes to strings + self._swing_list = [m.name.capitalize() for m in supported_swing_modes] + + # Dump all supported modes for debug + _LOGGER.debug("Supported operational modes: '%s'.", + self._operation_list) + _LOGGER.debug("Supported fan modes: '%s'.", self._swing_list) + _LOGGER.debug("Supported swing modes: '%s'.", self._swing_list) # Attempt to load min/max target temperatures self._min_temperature = getattr( @@ -112,6 +134,7 @@ def __init__(self, hass, device, options: dict): self._changed = False async def apply_changes(self) -> None: + """Apply changes to the device.""" if not self._changed: return @@ -124,8 +147,7 @@ async def apply_changes(self) -> None: self._changed = False async def async_update(self) -> None: - """Retrieve latest state from the appliance if no changes made, - otherwise update the remote device state.""" + """Update the device state.""" if self._changed: await self._device.apply() self._changed = False @@ -133,7 +155,7 @@ async def async_update(self) -> None: await self._device.refresh() async def async_added_to_hass(self) -> None: - """Run when entity about to be added.""" + """Callback when entity is about to be added.""" await super().async_added_to_hass() # Populate data ASAP @@ -151,32 +173,32 @@ def device_info(self) -> dict: @property def available(self) -> bool: - """Checks if the appliance is available for commands.""" + """Check if the device is available.""" return self._device.online @property def supported_features(self) -> int: - """Return the list of supported features.""" + """Return the supported features.""" return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE | SUPPORT_PRESET_MODE @property def target_temperature_step(self) -> float: - """Return the supported step of target temperature.""" + """Return the supported target temperature step.""" return self._target_temperature_step @property def hvac_modes(self) -> list: - """Return the list of available operation modes.""" + """Return the supported operation modes.""" return self._operation_list @property def fan_modes(self) -> list: - """Return the list of available fan modes.""" + """Return the supported fan modes.""" return self._fan_list @property def swing_modes(self) -> list: - """List of available swing modes.""" + """Return the supported swing modes.""" return self._swing_list @property @@ -191,6 +213,7 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str: + """Return the unique ID of this device.""" return f"{self._device.id}" @property @@ -210,33 +233,35 @@ def current_temperature(self) -> float: @property def target_temperature(self) -> float: - """Return the temperature we try to reach.""" + """Return the current target temperature.""" return self._device.target_temperature @property def hvac_mode(self) -> str: - """Return current operation ie. heat, cool, idle.""" + """Return current HVAC mode.""" if self._include_off_as_state and not self._device.power_state: - return "off" - return self._device.operational_mode.name + return HVACMode.OFF + + # TODO What else to default to? + return _OPERATIONAL_MODE_TO_HVAC_MODE.get(self._device.operational_mode, HVACMode.OFF) @property def fan_mode(self) -> str: - """Return the fan setting.""" - return self._device.fan_speed.name + """Return the current fan speed mode.""" + return self._device.fan_speed.name.capitalize() @property def swing_mode(self) -> str: - """Return the swing setting.""" - return self._device.swing_mode.name + """Return the current swing mode.""" + return self._device.swing_mode.name.capitalize() @property def is_on(self) -> bool: - """Return true if the device is on.""" + """Check if the device is on.""" return self._device.power_state async def async_set_temperature(self, **kwargs) -> None: - """Set new target temperatures.""" + """Set a new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: # grab temperature from front end UI temp = kwargs.get(ATTR_TEMPERATURE) @@ -250,31 +275,35 @@ async def async_set_temperature(self, **kwargs) -> None: await self.apply_changes() async def async_set_swing_mode(self, swing_mode) -> None: - """Set swing mode.""" - self._device.swing_mode = ac.swing_mode_enum[swing_mode] + """Set the swing mode.""" + self._device.swing_mode = AC.SwingMode.get_from_name( + swing_mode.upper(), self._device.swing_mode) self._changed = True await self.apply_changes() async def async_set_fan_mode(self, fan_mode) -> None: - """Set fan mode.""" - """Fix key error when calling from HomeKit""" - fan_mode = fan_mode.capitalize() - self._device.fan_speed = ac.fan_speed_enum[fan_mode] + """Set the fan mode.""" + self._device.fan_speed = AC.FanSpeed.get_from_name( + fan_mode.upper(), self._device.fan_speed) self._changed = True await self.apply_changes() async def async_set_hvac_mode(self, hvac_mode) -> None: - """Set hvac mode.""" - if self._include_off_as_state and hvac_mode == "off": + """Set the HVAC mode.""" + if self._include_off_as_state and hvac_mode == HVACMode.OFF: self._device.power_state = False else: if self._include_off_as_state: self._device.power_state = True - self._device.operational_mode = ac.operational_mode_enum[hvac_mode] + + self._device.operational_mode = _HVAC_MODE_TO_OPERATIONAL_MODE.get( + hvac_mode, self._device.operational_mode) + self._changed = True await self.apply_changes() async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" # TODO Assuming these are all mutually exclusive self._device.eco_mode = False self._device.turbo_mode = False @@ -296,25 +325,27 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: @property def preset_modes(self) -> list: + """Return the supported preset modes.""" # TODO could check for supports_eco and supports_turbo modes = [PRESET_NONE, PRESET_BOOST] # Add away preset if in heat and supports freeze protection - if getattr(self._device, "supports_freeze_protection_mode", False) and self._device.operational_mode == ac.operational_mode_enum.heat: + if getattr(self._device, "supports_freeze_protection_mode", False) and self._device.operational_mode == AC.OperationalMode.HEAT: modes.append(PRESET_AWAY) # Add eco preset in cool, dry and auto - if self._device.operational_mode in [ac.operational_mode_enum.dry, ac.operational_mode_enum.cool, ac.operational_mode_enum.auto]: + if self._device.operational_mode in [AC.OperationalMode.DRY, AC.OperationalMode.COOL, AC.OperationalMode.AUTO]: modes.append(PRESET_ECO) # Add sleep preset in heat, cool or auto - if self._device.operational_mode in [ac.operational_mode_enum.heat, ac.operational_mode_enum.cool, ac.operational_mode_enum.auto]: + if self._device.operational_mode in [AC.OperationalMode.HEAT, AC.OperationalMode.COOL, AC.OperationalMode.AUTO]: modes.append(PRESET_SLEEP) return modes @property def preset_mode(self) -> str: + """Get the current preset mode.""" if self._device.eco_mode: return PRESET_ECO elif self._device.turbo_mode: diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py index f8ba03f..d6ce81e 100644 --- a/custom_components/midea_ac/config_flow.py +++ b/custom_components/midea_ac/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from msmart.device import air_conditioning as ac +from msmart.device import AirConditioner as AC from msmart.discover import Discover # Local constants @@ -165,7 +165,7 @@ async def _test_manual_connection(self, config) -> ac | None: id = config.get(CONF_ID) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - device = ac(host, int(id), port) + device = AC(ip=host, port=port, device_id=int(id)) # Configure token and key as needed token = config.get(CONF_TOKEN) From aaa3f81a4e9cb6b6c2d032feb34893450949d889 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Thu, 24 Aug 2023 20:48:39 -0600 Subject: [PATCH 06/13] Rename prompt_tone to beep to align with msmart-ng --- custom_components/midea_ac/climate.py | 4 ++-- custom_components/midea_ac/config_flow.py | 8 ++++---- custom_components/midea_ac/const.py | 2 +- custom_components/midea_ac/translations/en.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index d7181d8..78cdffb 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -23,7 +23,7 @@ from . import helpers # Local constants from .const import (CONF_ADDITIONAL_OPERATION_MODES, CONF_INCLUDE_OFF_AS_STATE, - CONF_KEEP_LAST_KNOWN_ONLINE_STATE, CONF_PROMPT_TONE, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE, CONF_BEEP, CONF_TEMP_STEP, CONF_USE_FAN_ONLY_WORKAROUND, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -81,7 +81,7 @@ def __init__(self, hass, device, options: dict): self._device = device # Apply options - self._device.prompt_tone = options.get(CONF_PROMPT_TONE) + self._device.beep = options.get(CONF_BEEP) self._device.keep_last_known_online_state = options.get( CONF_KEEP_LAST_KNOWN_ONLINE_STATE) diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py index d6ce81e..8c5942d 100644 --- a/custom_components/midea_ac/config_flow.py +++ b/custom_components/midea_ac/config_flow.py @@ -15,11 +15,11 @@ # Local constants from .const import (CONF_ADDITIONAL_OPERATION_MODES, CONF_INCLUDE_OFF_AS_STATE, CONF_KEEP_LAST_KNOWN_ONLINE_STATE, CONF_KEY, - CONF_PROMPT_TONE, CONF_TEMP_STEP, + CONF_BEEP, CONF_TEMP_STEP, CONF_USE_FAN_ONLY_WORKAROUND, DOMAIN) _DEFAULT_OPTIONS = { - CONF_PROMPT_TONE: True, + CONF_BEEP: True, CONF_TEMP_STEP: 1.0, CONF_INCLUDE_OFF_AS_STATE: True, CONF_USE_FAN_ONLY_WORKAROUND: False, @@ -214,8 +214,8 @@ async def async_step_init(self, user_input=None) -> FlowResult: options = self.config_entry.options data_schema = vol.Schema({ - vol.Optional(CONF_PROMPT_TONE, - default=options.get(CONF_PROMPT_TONE, True)): cv.boolean, + vol.Optional(CONF_BEEP, + default=options.get(CONF_BEEP, True)): cv.boolean, vol.Optional(CONF_TEMP_STEP, default=options.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), vol.Optional(CONF_INCLUDE_OFF_AS_STATE, diff --git a/custom_components/midea_ac/const.py b/custom_components/midea_ac/const.py index 2033469..f475ee4 100644 --- a/custom_components/midea_ac/const.py +++ b/custom_components/midea_ac/const.py @@ -1,7 +1,7 @@ DOMAIN = "midea_ac" CONF_KEY = "k1" -CONF_PROMPT_TONE = "prompt_tone" +CONF_BEEP = "prompt_tone" CONF_TEMP_STEP = "temp_step" CONF_INCLUDE_OFF_AS_STATE = "include_off_as_state" CONF_USE_FAN_ONLY_WORKAROUND = "use_fan_only_workaround" diff --git a/custom_components/midea_ac/translations/en.json b/custom_components/midea_ac/translations/en.json index ce930ad..6af3a33 100644 --- a/custom_components/midea_ac/translations/en.json +++ b/custom_components/midea_ac/translations/en.json @@ -42,7 +42,7 @@ "step": { "init": { "data": { - "prompt_tone": "Prompt Tone", + "prompt_tone": "Beep", "temp_step": "Temperature Step", "include_off_as_state": "Include \"Off\" State", "use_fan_only_workaround": "Use Fan-only Workaround", From 7e5b25f1677d8750286092fec6a9f6692c115239 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Thu, 24 Aug 2023 20:49:06 -0600 Subject: [PATCH 07/13] Copy en.json to strings.json in case its ever used --- custom_components/midea_ac/strings.json | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/custom_components/midea_ac/strings.json b/custom_components/midea_ac/strings.json index 0486f1a..6af3a33 100644 --- a/custom_components/midea_ac/strings.json +++ b/custom_components/midea_ac/strings.json @@ -2,42 +2,47 @@ "config": { "step": { "user": { - "title": "Configure Midea Smart AC Device", + "description": "Select how to add a device.", + "menu_options": { + "discover": "Discover device", + "manual": "Manually configure" + } + }, + "discover": { + "data": { + "host": "Host" + }, + "description": "Leave the host blank to discover device(s) on the network." + }, + "manual": { "description": "Enter information for your device.", "data": { "id": "ID", "host": "Host", "port": "Port", "token": "Token", - "k1": "K1", - "prompt_tone": "Prompt Tone", - "temp_step": "Temperature Step", - "include_off_as_state": "Include \"Off\" State", - "use_fan_only_workaround": "Use Fan-only Workaround", - "keep_last_known_online_state": "Keep Last Known State", - "additional_operation_modes": "Additional Operation Modes" + "k1": "Key" }, "data_description": { "token": "Token for V3 devices", - "k1": "K1 for V3 devices", - "prompt_tone": "Enable the beep when sending commands", - "temp_step": "Step size for temperature set point" + "k1": "Key for V3 devices" } } }, "abort":{ - "already_configured": "The device ID has already been configured." + "already_configured": "The device has already been configured.", + "no_devices_found": "No device(s) found on the network." }, "error":{ - "cannot_connect":"A connection could not be made with these settings." + "cannot_connect":"A connection could not be made with these settings.", + "no_devices_found": "No device(s) found on the network." } }, "options": { "step": { "init": { - "title": "Options for Midea Smart AC Device", "data": { - "prompt_tone": "Prompt Tone", + "prompt_tone": "Beep", "temp_step": "Temperature Step", "include_off_as_state": "Include \"Off\" State", "use_fan_only_workaround": "Use Fan-only Workaround", From 8dd64b636dfc3ed4e39acf4de5d147fd86b43a8c Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 25 Aug 2023 18:07:50 -0600 Subject: [PATCH 08/13] Update README for auto configure and tweak verbiage of translations --- README.md | 17 ++++++++++++----- custom_components/midea_ac/strings.json | 14 +++++++------- custom_components/midea_ac/translations/en.json | 14 +++++++------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5153408..2f096f9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Midea is an OEM for many brands including: Hualing, Senville, Klimaire, AirCon, Century, Pridiom, Thermocore, Comfee, Alpine Home Air, Artel, Beko, Electrolux, Galactic, Idea, Inventor, Kaisai, Mitsui, Mr. Cool, Neoclima, Olimpia Splendid, Pioneer, QLIMA, Rotenso, Royal Clima, Qzen, Toshiba, Carrier, Goodman, Friedrich, Samsung, Kenmore, Trane, Lennox, LG and more. ## Features +* Automatic device discovery and configuration via the GUI. * Device capability detection. Only supported modes and functions are available. * Support for sleep, eco, boost (turbo) and away (freeze protection) presets. * Switch entity for device display. @@ -26,7 +27,13 @@ Hualing, Senville, Klimaire, AirCon, Century, Pridiom, Thermocore, Comfee, Alpin ## Configuration & Options Midea Smart AC is configured via the GUI. See [the HA docs](https://www.home-assistant.io/getting-started/integration/) for more details. -The device ID, IP, and port are required for all devices. V3 devices require an additional token and K1 parameter. Currently this needs to be [acquired manually](#getting-device-info). +Devices can be automatically discovered and configured or manually configured. + +### Automatic Configuration +For automatic configuration, select "Discover devices". Leave the host field blank to search the local network, or provide an IP/hostname to configure a specific device. + +### Manual Configuration +For manual configuration, select "Configure manually". Enter the device ID, IP, and port. V3 devices require the token and key parameter. This information must be [acquired manually](#getting-device-info). #### Integration Configuration --- @@ -36,22 +43,22 @@ Name | Description | Required | Example **Host** | Device IP address | Yes| 192.168.1.100 **Port** | Device port | Yes | 6444 **Token** | Device token | For V3 devices | ACEDDA53831AE5DC... (Length 128) -**K1** | Device K1 | For V3 devices | CFFA10FC... (Length 64) +**Key** | Device key | For V3 devices | CFFA10FC... (Length 64) #### Integration Options --- Name | Description :--- | :--- -**Prompt Tone** | Enable beep on setting changes. +**Beep** | Enable beep on setting changes. **Temperature Step** | Step size for temperature set point. **Include "Off" State** | Include "Off" as a valid device state. **Use Fan-only Workaround** | Enable this option if device updates cause the device to turn on and switch to fan-only. **Keep Last Known State** | Enable this option if there are too many `Unavailable` reports in the log. ## Getting Device Info -Use the `midea-discover` command from midea-msmart to obtain device information. +Use the `midea-discover` command from msmart-ng to obtain device information. ```shell -pip install git+https://github.com/mill1000/midea-msmart.git +pip install msmart-ng midea-discover ``` _Note: Only devices of type 0xAC are supported. Ensure the supported property is True._ diff --git a/custom_components/midea_ac/strings.json b/custom_components/midea_ac/strings.json index 6af3a33..de8515a 100644 --- a/custom_components/midea_ac/strings.json +++ b/custom_components/midea_ac/strings.json @@ -5,7 +5,7 @@ "description": "Select how to add a device.", "menu_options": { "discover": "Discover device", - "manual": "Manually configure" + "manual": "Configure manually" } }, "discover": { @@ -29,12 +29,12 @@ } } }, - "abort":{ + "abort": { "already_configured": "The device has already been configured.", "no_devices_found": "No device(s) found on the network." }, - "error":{ - "cannot_connect":"A connection could not be made with these settings.", + "error": { + "cannot_connect": "A connection could not be made with these settings.", "no_devices_found": "No device(s) found on the network." } }, @@ -42,7 +42,7 @@ "step": { "init": { "data": { - "prompt_tone": "Beep", + "prompt_tone": "Enable Beep", "temp_step": "Temperature Step", "include_off_as_state": "Include \"Off\" State", "use_fan_only_workaround": "Use Fan-only Workaround", @@ -50,8 +50,8 @@ "additional_operation_modes": "Additional Operation Modes" }, "data_description": { - "prompt_tone": "Enable the beep when sending commands", - "temp_step": "Step size for temperature set point" + "temp_step": "Step size for temperature set point", + "additional_operation_modes": "Specify additional operational modes" } } } diff --git a/custom_components/midea_ac/translations/en.json b/custom_components/midea_ac/translations/en.json index 6af3a33..de8515a 100644 --- a/custom_components/midea_ac/translations/en.json +++ b/custom_components/midea_ac/translations/en.json @@ -5,7 +5,7 @@ "description": "Select how to add a device.", "menu_options": { "discover": "Discover device", - "manual": "Manually configure" + "manual": "Configure manually" } }, "discover": { @@ -29,12 +29,12 @@ } } }, - "abort":{ + "abort": { "already_configured": "The device has already been configured.", "no_devices_found": "No device(s) found on the network." }, - "error":{ - "cannot_connect":"A connection could not be made with these settings.", + "error": { + "cannot_connect": "A connection could not be made with these settings.", "no_devices_found": "No device(s) found on the network." } }, @@ -42,7 +42,7 @@ "step": { "init": { "data": { - "prompt_tone": "Beep", + "prompt_tone": "Enable Beep", "temp_step": "Temperature Step", "include_off_as_state": "Include \"Off\" State", "use_fan_only_workaround": "Use Fan-only Workaround", @@ -50,8 +50,8 @@ "additional_operation_modes": "Additional Operation Modes" }, "data_description": { - "prompt_tone": "Enable the beep when sending commands", - "temp_step": "Step size for temperature set point" + "temp_step": "Step size for temperature set point", + "additional_operation_modes": "Specify additional operational modes" } } } From f3bdd92ce48816ad69d084c89e3d428071132844 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 25 Aug 2023 19:03:57 -0600 Subject: [PATCH 09/13] Only show supported presets --- custom_components/midea_ac/climate.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index 608785f..e1ea8aa 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -327,19 +327,28 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: @property def preset_modes(self) -> list: """Return the supported preset modes.""" - # TODO could check for supports_eco and supports_turbo - modes = [PRESET_NONE, PRESET_BOOST] + modes = [PRESET_NONE] + + # Add turbo/boost if supported by the device + if getattr(self._device, "supports_turbo_mode", False): + modes.append(PRESET_BOOST) # Add away preset if in heat and supports freeze protection - if getattr(self._device, "supports_freeze_protection_mode", False) and self._device.operational_mode == AC.OperationalMode.HEAT: + if getattr(self._device, "supports_freeze_protection_mode", False) + and self._device.operational_mode == AC.OperationalMode.HEAT: modes.append(PRESET_AWAY) - # Add eco preset in cool, dry and auto - if self._device.operational_mode in [AC.OperationalMode.DRY, AC.OperationalMode.COOL, AC.OperationalMode.AUTO]: + # Add eco preset in cool, dry and auto if supported + if (getattr(self._device, "supports_eco_mode", False) + and self._device.operational_mode in [AC.OperationalMode.AUTO, + AC.OperationalMode.COOL, + AC.OperationalMode.DRY]): modes.append(PRESET_ECO) # Add sleep preset in heat, cool or auto - if self._device.operational_mode in [AC.OperationalMode.HEAT, AC.OperationalMode.COOL, AC.OperationalMode.AUTO]: + if self._device.operational_mode in [AC.OperationalMode.AUTO, + AC.OperationalMode.COOL, + AC.OperationalMode.HEAT]: modes.append(PRESET_SLEEP) return modes From 937739c6254018265bff71ac1ced31e968c8342b Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 25 Aug 2023 19:13:22 -0600 Subject: [PATCH 10/13] Fix syntax error --- custom_components/midea_ac/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index e1ea8aa..a507bbf 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -334,8 +334,8 @@ def preset_modes(self) -> list: modes.append(PRESET_BOOST) # Add away preset if in heat and supports freeze protection - if getattr(self._device, "supports_freeze_protection_mode", False) - and self._device.operational_mode == AC.OperationalMode.HEAT: + if (getattr(self._device, "supports_freeze_protection_mode", False) + and self._device.operational_mode == AC.OperationalMode.HEAT): modes.append(PRESET_AWAY) # Add eco preset in cool, dry and auto if supported From 1e3ed8c415fe362c9f557ca99e69668afb4ba682 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 25 Aug 2023 19:23:53 -0600 Subject: [PATCH 11/13] Only display boost reset in auto, cool and heat --- custom_components/midea_ac/climate.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index a507bbf..fc19460 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -329,11 +329,7 @@ def preset_modes(self) -> list: """Return the supported preset modes.""" modes = [PRESET_NONE] - # Add turbo/boost if supported by the device - if getattr(self._device, "supports_turbo_mode", False): - modes.append(PRESET_BOOST) - - # Add away preset if in heat and supports freeze protection + # Add away preset in heat if it supports freeze protection if (getattr(self._device, "supports_freeze_protection_mode", False) and self._device.operational_mode == AC.OperationalMode.HEAT): modes.append(PRESET_AWAY) @@ -345,15 +341,20 @@ def preset_modes(self) -> list: AC.OperationalMode.DRY]): modes.append(PRESET_ECO) - # Add sleep preset in heat, cool or auto + # Add sleep and/or turbo preset in heat, cool or auto if self._device.operational_mode in [AC.OperationalMode.AUTO, AC.OperationalMode.COOL, AC.OperationalMode.HEAT]: + # TODO Always add sleep modes.append(PRESET_SLEEP) + # Add turbo/boost if supported by the device + if (getattr(self._device, "supports_turbo_mode", False) and : + modes.append(PRESET_BOOST) + return modes - @property + @ property def preset_mode(self) -> str: """Get the current preset mode.""" if self._device.eco_mode: @@ -369,22 +370,22 @@ def preset_mode(self) -> str: async def async_turn_on(self) -> None: """Turn on.""" - self._device.power_state = True - self._changed = True + self._device.power_state=True + self._changed=True await self.apply_changes() async def async_turn_off(self) -> None: """Turn off.""" - self._device.power_state = False - self._changed = True + self._device.power_state=False + self._changed=True await self.apply_changes() - @property + @ property def min_temp(self) -> float: """Return the minimum temperature.""" return self._min_temperature - @property + @ property def max_temp(self) -> float: """Return the maximum temperature.""" return self._max_temperature From 3fd4ef755d798890b7b0913c0d5cdb41e5319eba Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Fri, 25 Aug 2023 19:25:37 -0600 Subject: [PATCH 12/13] JFC test before you commit... --- custom_components/midea_ac/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index fc19460..533e4c4 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -349,7 +349,7 @@ def preset_modes(self) -> list: modes.append(PRESET_SLEEP) # Add turbo/boost if supported by the device - if (getattr(self._device, "supports_turbo_mode", False) and : + if getattr(self._device, "supports_turbo_mode", False): modes.append(PRESET_BOOST) return modes From d430e10cef6d0d314b0289d847362f1715a94b60 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Sat, 26 Aug 2023 19:26:18 -0600 Subject: [PATCH 13/13] Fix autopep8 issues --- custom_components/midea_ac/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py index 533e4c4..138b294 100644 --- a/custom_components/midea_ac/climate.py +++ b/custom_components/midea_ac/climate.py @@ -370,14 +370,14 @@ def preset_mode(self) -> str: async def async_turn_on(self) -> None: """Turn on.""" - self._device.power_state=True - self._changed=True + self._device.power_state = True + self._changed = True await self.apply_changes() async def async_turn_off(self) -> None: """Turn off.""" - self._device.power_state=False - self._changed=True + self._device.power_state = False + self._changed = True await self.apply_changes() @ property