diff --git a/custom_components/midea_auto_codec/__init__.py b/custom_components/midea_auto_codec/__init__.py index 919dad6..5e7d190 100644 --- a/custom_components/midea_auto_codec/__init__.py +++ b/custom_components/midea_auto_codec/__init__.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ) from .core.logger import MideaLogger from .core.device import MiedaDevice +from .data_coordinator import MideaDataUpdateCoordinator from .const import ( DOMAIN, DEVICES, @@ -98,29 +99,24 @@ def register_services(hass: HomeAssistant): attributes = service.data.get("attributes") MideaLogger.debug(f"Service called: set_attributes, device_id: {device_id}, attributes: {attributes}") try: - device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) + coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator") except KeyError: MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.") return - if device: - device.set_attributes(attributes) + if coordinator: + await coordinator.async_set_attributes(attributes) async def async_send_command(service: ServiceCall): device_id = service.data.get("device_id") cmd_type = service.data.get("cmd_type") cmd_body = service.data.get("cmd_body") try: - cmd_body = bytearray.fromhex(cmd_body) - except ValueError: - MideaLogger.error(f"Failed to call service set_attributes: invalid cmd_body, a hexadecimal string required") - return - try: - device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) + coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator") except KeyError: - MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.") + MideaLogger.error(f"Failed to call service send_command: the device {device_id} isn't exist.") return - if device: - device.send_command(cmd_type, cmd_body) + if coordinator: + await coordinator.async_send_command(cmd_type, cmd_body) hass.services.async_register( DOMAIN, @@ -222,9 +218,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): hass.data[DOMAIN] = {} if DEVICES not in hass.data[DOMAIN]: hass.data[DOMAIN][DEVICES] = {} + + # Create data coordinator + coordinator = MideaDataUpdateCoordinator(hass, config_entry, device) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][DEVICES][device_id] = {} hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device + hass.data[DOMAIN][DEVICES][device_id]["coordinator"] = coordinator hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {} + config = load_device_config(hass, device_type, sn8) if config is not None and len(config) > 0: queries = config.get("queries") @@ -239,6 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer") hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale") hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES) + for platform in ALL_PLATFORM: hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, platform)) diff --git a/custom_components/midea_auto_codec/binary_sensor.py b/custom_components/midea_auto_codec/binary_sensor.py index edc44ca..710e026 100644 --- a/custom_components/midea_auto_codec/binary_sensor.py +++ b/custom_components/midea_auto_codec/binary_sensor.py @@ -5,58 +5,115 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( Platform, CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES + CONF_ENTITIES, CONF_DEVICE ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN, DEVICES ) -from .midea_entities import MideaBinaryBaseEntity +from .midea_entity import MideaEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensor entities for Midea devices.""" device_id = config_entry.data.get(CONF_DEVICE_ID) - device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.BINARY_SENSOR) - devs = [MideaDeviceStatusSensorEntity(device, manufacturer, rationale,"Status", {})] - if entities is not None: + device_data = hass.data[DOMAIN][DEVICES][device_id] + coordinator = device_data.get("coordinator") + device = device_data.get(CONF_DEVICE) + manufacturer = device_data.get("manufacturer") + rationale = device_data.get("rationale") + entities = device_data.get(CONF_ENTITIES, {}).get(Platform.BINARY_SENSOR, {}) + + devs = [MideaDeviceStatusSensorEntity(coordinator, device, manufacturer, rationale, "Status", {})] + if entities: for entity_key, config in entities.items(): - devs.append(MideaBinarySensorEntity(device, manufacturer, rationale, entity_key, config)) + devs.append(MideaBinarySensorEntity( + coordinator, device, manufacturer, rationale, entity_key, config + )) async_add_entities(devs) -class MideaDeviceStatusSensorEntity(MideaBinaryBaseEntity, BinarySensorEntity): +class MideaDeviceStatusSensorEntity(MideaEntity, BinarySensorEntity): + """Device status binary sensor.""" + + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + ) + self._device = device + self._manufacturer = manufacturer + self._rationale = rationale + self._entity_key = entity_key + self._config = config + + @property + def entity_id_suffix(self) -> str: + """Return the suffix for entity ID.""" + return "status" @property def device_class(self): + """Return the device class.""" return BinarySensorDeviceClass.CONNECTIVITY @property def icon(self): + """Return the icon.""" return "mdi:devices" @property def is_on(self): - return self._device.connected - - @property - def available(self): - return True + """Return if the device is connected.""" + return self.coordinator.data.connected @property def extra_state_attributes(self) -> dict: - return self._device.attributes - - def update_state(self, status): - try: - self.schedule_update_ha_state() - except Exception as e: - pass + """Return extra state attributes.""" + return self.device_attributes -class MideaBinarySensorEntity(MideaBinaryBaseEntity, BinarySensorEntity): - pass +class MideaBinarySensorEntity(MideaEntity, BinarySensorEntity): + """Generic binary sensor entity.""" + + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + ) + self._device = device + self._manufacturer = manufacturer + self._rationale = rationale + self._entity_key = entity_key + self._config = config + + @property + def entity_id_suffix(self) -> str: + """Return the suffix for entity ID.""" + return f"binary_sensor_{self._entity_key}" + + @property + def is_on(self): + """Return if the binary sensor is on.""" + value = self.device_attributes.get(self._entity_key) + if isinstance(value, bool): + return value + return value == 1 or value == "on" or value == "true" diff --git a/custom_components/midea_auto_codec/climate.py b/custom_components/midea_auto_codec/climate.py index 610408b..c79bdf0 100644 --- a/custom_components/midea_auto_codec/climate.py +++ b/custom_components/midea_auto_codec/climate.py @@ -8,33 +8,59 @@ from homeassistant.const import ( Platform, CONF_DEVICE_ID, CONF_ENTITIES, - CONF_DEVICE, - ATTR_TEMPERATURE + ATTR_TEMPERATURE, CONF_DEVICE ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN, DEVICES ) -from .midea_entities import MideaEntity, Rationale +from .midea_entity import MideaEntity +from .midea_entities import Rationale -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate entities for Midea devices.""" device_id = config_entry.data.get(CONF_DEVICE_ID) - device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.CLIMATE) + device_data = hass.data[DOMAIN][DEVICES][device_id] + coordinator = device_data.get("coordinator") + device = device_data.get(CONF_DEVICE) + manufacturer = device_data.get("manufacturer") + rationale = device_data.get("rationale") + entities = device_data.get(CONF_ENTITIES, {}).get(Platform.CLIMATE, {}) + devs = [] - if entities is not None: + if entities: for entity_key, config in entities.items(): - devs.append(MideaClimateEntity(device, manufacturer, rationale, entity_key, config)) + devs.append(MideaClimateEntity( + coordinator, device, manufacturer, rationale, entity_key, config + )) async_add_entities(devs) class MideaClimateEntity(MideaEntity, ClimateEntity): - def __init__(self, device, manufacturer, rationale, entity_key, config): - super().__init__(device, manufacturer, rationale, entity_key, config) + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + ) + self._device = device + self._manufacturer = manufacturer + self._rationale = rationale + self._entity_key = entity_key + self._config = config self._key_power = self._config.get("power") self._key_hvac_modes = self._config.get("hvac_modes") self._key_preset_modes = self._config.get("preset_modes") @@ -65,30 +91,30 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): @property def current_temperature(self): - return self._device.get_attribute(self._key_current_temperature) + return self.device_attributes.get(self._key_current_temperature) @property def target_temperature(self): if isinstance(self._key_target_temperature, list): - temp_int = self._device.get_attribute(self._key_target_temperature[0]) - tem_dec = self._device.get_attribute(self._key_target_temperature[1]) + temp_int = self.device_attributes.get(self._key_target_temperature[0]) + tem_dec = self.device_attributes.get(self._key_target_temperature[1]) if temp_int is not None and tem_dec is not None: return temp_int + tem_dec return None else: - return self._device.get_attribute(self._key_target_temperature) + return self.device_attributes.get(self._key_target_temperature) @property def min_temp(self): if isinstance(self._key_min_temp, str): - return float(self._device.get_attribute(self._key_min_temp)) + return float(self.device_attributes.get(self._key_min_temp, 16)) else: return float(self._key_min_temp) @property def max_temp(self): if isinstance(self._key_max_temp, str): - return float(self._device.get_attribute(self._key_max_temp)) + return float(self.device_attributes.get(self._key_max_temp, 30)) else: return float(self._key_max_temp) @@ -140,13 +166,13 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): def is_aux_heat(self): return self._get_status_on_off(self._key_aux_heat) - def turn_on(self): - self._set_status_on_off(self._key_power, True) + async def async_turn_on(self): + await self._async_set_status_on_off(self._key_power, True) - def turn_off(self): - self._set_status_on_off(self._key_power, False) + async def async_turn_off(self): + await self._async_set_status_on_off(self._key_power, False) - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): if ATTR_TEMPERATURE not in kwargs: return temperature = kwargs.get(ATTR_TEMPERATURE) @@ -162,32 +188,68 @@ class MideaClimateEntity(MideaEntity, ClimateEntity): new_status[self._key_target_temperature[1]] = temp_dec else: new_status[self._key_target_temperature] = temperature - self._device.set_attributes(new_status) + await self.async_set_attributes(new_status) - def set_fan_mode(self, fan_mode: str): + async def async_set_fan_mode(self, fan_mode: str): new_status = self._key_fan_modes.get(fan_mode) - self._device.set_attributes(new_status) + await self.async_set_attributes(new_status) - def set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str): new_status = self._key_preset_modes.get(preset_mode) - self._device.set_attributes(new_status) + await self.async_set_attributes(new_status) - def set_hvac_mode(self, hvac_mode: str): + async def async_set_hvac_mode(self, hvac_mode: str): new_status = self._key_hvac_modes.get(hvac_mode) - self._device.set_attributes(new_status) + await self.async_set_attributes(new_status) - def set_swing_mode(self, swing_mode: str): + async def async_set_swing_mode(self, swing_mode: str): new_status = self._key_swing_modes.get(swing_mode) - self._device.set_attributes(new_status) + await self.async_set_attributes(new_status) - def turn_aux_heat_on(self) -> None: - self._set_status_on_off(self._key_aux_heat, True) + async def async_turn_aux_heat_on(self) -> None: + await self._async_set_status_on_off(self._key_aux_heat, True) - def turn_aux_heat_off(self) -> None: - self._set_status_on_off(self._key_aux_heat, False) + async def async_turn_aux_heat_off(self) -> None: + await self._async_set_status_on_off(self._key_aux_heat, False) - def update_state(self, status): - try: - self.schedule_update_ha_state() - except Exception as e: - pass + def _get_status_on_off(self, key): + """Get on/off status from device attributes.""" + if key is None: + return False + value = self.device_attributes.get(key) + if isinstance(value, bool): + return value + return value == 1 or value == "on" or value == "true" + + async def _async_set_status_on_off(self, key, value): + """Set on/off status for device attribute.""" + if key is None: + return + await self.async_set_attribute(key, value) + + def _dict_get_selected(self, dict_config, rationale=Rationale.EQUAL): + """Get selected value from dictionary configuration.""" + if dict_config is None: + return None + + for key, config in dict_config.items(): + if isinstance(config, dict): + # Check if all conditions match + match = True + for attr_key, attr_value in config.items(): + device_value = self.device_attributes.get(attr_key) + if rationale == Rationale.EQUAL: + if device_value != attr_value: + match = False + break + elif rationale == Rationale.LESS: + if device_value >= attr_value: + match = False + break + elif rationale == Rationale.GREATER: + if device_value <= attr_value: + match = False + break + if match: + return key + return None diff --git a/custom_components/midea_auto_codec/config_flow.py b/custom_components/midea_auto_codec/config_flow.py index 536f884..3d91f6f 100644 --- a/custom_components/midea_auto_codec/config_flow.py +++ b/custom_components/midea_auto_codec/config_flow.py @@ -86,10 +86,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): account=account, password=password ) - if await self._cloud.login(): - return await self.async_step_home() - else: - return await self.async_step_user(error="account_invalid") + try: + if await self._cloud.login(): + return await self.async_step_home() + else: + return await self.async_step_user(error="account_invalid") + except Exception as e: + _LOGGER.error(f"Login error: {e}") + return await self.async_step_user(error="login_failed") if user_input is not None: if self._cloud is None: self._cloud = get_midea_cloud( @@ -98,16 +102,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): account=user_input[CONF_ACCOUNT], password=user_input[CONF_PASSWORD] ) - if await self._cloud.login(): - return self.async_create_entry( - title=f"{user_input[CONF_ACCOUNT]}", - data={ - CONF_TYPE: CONF_ACCOUNT, - CONF_ACCOUNT: user_input[CONF_ACCOUNT], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_SERVER: user_input[CONF_SERVER] - }) - else: + try: + if await self._cloud.login(): + return self.async_create_entry( + title=f"{user_input[CONF_ACCOUNT]}", + data={ + CONF_TYPE: CONF_ACCOUNT, + CONF_ACCOUNT: user_input[CONF_ACCOUNT], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_SERVER: user_input[CONF_SERVER] + }) + else: + self._cloud = None + return await self.async_step_user(error="login_failed") + except Exception as e: + _LOGGER.error(f"Login error: {e}") self._cloud = None return await self.async_step_user(error="login_failed") return self.async_show_form( diff --git a/custom_components/midea_auto_codec/core/device.py b/custom_components/midea_auto_codec/core/device.py index 775057b..ac18fef 100644 --- a/custom_components/midea_auto_codec/core/device.py +++ b/custom_components/midea_auto_codec/core/device.py @@ -326,6 +326,10 @@ class MiedaDevice(threading.Thread): def _device_connected(self, connected=True): self._connected = connected status = {"connected": connected} + if not connected: + MideaLogger.warning(f"Device {self._device_id} disconnected", self._device_id) + else: + MideaLogger.info(f"Device {self._device_id} connected", self._device_id) self._update_all(status) def _update_all(self, status): diff --git a/custom_components/midea_auto_codec/data_coordinator.py b/custom_components/midea_auto_codec/data_coordinator.py new file mode 100644 index 0000000..24e9346 --- /dev/null +++ b/custom_components/midea_auto_codec/data_coordinator.py @@ -0,0 +1,127 @@ +"""Data coordinator for Midea Auto Codec integration.""" + +import logging +from datetime import datetime, timedelta +from typing import NamedTuple + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .core.device import MiedaDevice + +_LOGGER = logging.getLogger(__name__) + + +class MideaDeviceData(NamedTuple): + """Data structure for Midea device state.""" + attributes: dict + available: bool + connected: bool + + +class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]): + """Data update coordinator for Midea devices.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + device: MiedaDevice, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=f"{device.device_name} ({device.device_id})", + update_method=self.poll_device_state, + update_interval=timedelta(seconds=30), + always_update=False, + ) + self.device = device + self.state_update_muted: CALLBACK_TYPE | None = None + self._device_id = device.device_id + + async def _async_setup(self) -> None: + """Set up the coordinator.""" + self.data = MideaDeviceData( + attributes=self.device.attributes, + available=self.device.connected, + connected=self.device.connected, + ) + + # Register for device updates + self.device.register_update(self._device_update_callback) + + def mute_state_update_for_a_while(self) -> None: + """Mute subscription for a while to avoid state bouncing.""" + if self.state_update_muted: + self.state_update_muted() + + @callback + def unmute(now: datetime) -> None: + self.state_update_muted = None + + self.state_update_muted = async_call_later(self.hass, 10, unmute) + + def _device_update_callback(self, status: dict) -> None: + """Callback for device status updates.""" + if self.state_update_muted: + return + + # Update device attributes + for key, value in status.items(): + if key in self.device.attributes: + self.device.attributes[key] = value + + # Update coordinator data + self.async_set_updated_data( + MideaDeviceData( + attributes=self.device.attributes, + available=self.device.connected, + connected=self.device.connected, + ) + ) + + async def poll_device_state(self) -> MideaDeviceData: + """Poll device state.""" + if self.state_update_muted: + return self.data + + try: + # The device handles its own polling, so we just return current state + return MideaDeviceData( + attributes=self.device.attributes, + available=self.device.connected, + connected=self.device.connected, + ) + except Exception as e: + _LOGGER.error(f"Error polling device state: {e}") + return MideaDeviceData( + attributes=self.device.attributes, + available=False, + connected=False, + ) + + async def async_set_attribute(self, attribute: str, value) -> None: + """Set a device attribute.""" + self.device.set_attribute(attribute, value) + self.mute_state_update_for_a_while() + self.async_update_listeners() + + async def async_set_attributes(self, attributes: dict) -> None: + """Set multiple device attributes.""" + self.device.set_attributes(attributes) + self.mute_state_update_for_a_while() + self.async_update_listeners() + + async def async_send_command(self, cmd_type: int, cmd_body: str) -> None: + """Send a command to the device.""" + try: + cmd_body_bytes = bytearray.fromhex(cmd_body) + self.device.send_command(cmd_type, cmd_body_bytes) + except ValueError as e: + _LOGGER.error(f"Invalid command body: {e}") + raise \ No newline at end of file diff --git a/custom_components/midea_auto_codec/midea_entity.py b/custom_components/midea_auto_codec/midea_entity.py new file mode 100644 index 0000000..9997c09 --- /dev/null +++ b/custom_components/midea_auto_codec/midea_entity.py @@ -0,0 +1,104 @@ +"""Base entity class for Midea Auto Codec integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .data_coordinator import MideaDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity): + """Base class for Midea entities.""" + + def __init__( + self, + coordinator: MideaDataUpdateCoordinator, + device_id: int, + device_name: str, + device_type: str, + sn: str, + sn8: str, + model: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._device_id = device_id + self._device_name = device_name + self._device_type = device_type + self._sn = sn + self._sn8 = sn8 + self._model = model + + self._attr_has_entity_name = True + self._attr_unique_id = f"{sn8}_{self.entity_id_suffix}" + self.entity_id_base = f"midea_{sn8.lower()}" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sn8)}, + model=model, + serial_number=sn, + manufacturer="Midea", + name=device_name, + ) + + # Debounced command publishing + self._debounced_publish_command = Debouncer( + hass=self.coordinator.hass, + logger=_LOGGER, + cooldown=2, + immediate=True, + background=True, + function=self._publish_command, + ) + + if self.coordinator.config_entry: + self.coordinator.config_entry.async_on_unload( + self._debounced_publish_command.async_shutdown + ) + + @property + def entity_id_suffix(self) -> str: + """Return the suffix for entity ID.""" + return "base" + + @property + def device_attributes(self) -> dict: + """Return device attributes.""" + return self.coordinator.data.attributes + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.data.available + + async def _publish_command(self) -> None: + """Publish commands to the device.""" + # This will be implemented by subclasses + pass + + async def publish_command_from_current_state(self) -> None: + """Publish commands to the device from current state.""" + self.coordinator.mute_state_update_for_a_while() + self.coordinator.async_update_listeners() + await self._debounced_publish_command.async_call() + + async def async_set_attribute(self, attribute: str, value: Any) -> None: + """Set a device attribute.""" + await self.coordinator.async_set_attribute(attribute, value) + + async def async_set_attributes(self, attributes: dict) -> None: + """Set multiple device attributes.""" + await self.coordinator.async_set_attributes(attributes) + + async def async_send_command(self, cmd_type: int, cmd_body: str) -> None: + """Send a command to the device.""" + await self.coordinator.async_send_command(cmd_type, cmd_body) diff --git a/custom_components/midea_auto_codec/sensor.py b/custom_components/midea_auto_codec/sensor.py index 9d77204..aaa48f9 100644 --- a/custom_components/midea_auto_codec/sensor.py +++ b/custom_components/midea_auto_codec/sensor.py @@ -2,31 +2,67 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( Platform, CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES + CONF_ENTITIES, CONF_DEVICE ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + from .const import ( DOMAIN, DEVICES ) -from .midea_entities import MideaEntity +from .midea_entity import MideaEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities for Midea devices.""" device_id = config_entry.data.get(CONF_DEVICE_ID) - device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SENSOR) + device_data = hass.data[DOMAIN][DEVICES][device_id] + coordinator = device_data.get("coordinator") + device = device_data.get(CONF_DEVICE) + manufacturer = device_data.get("manufacturer") + rationale = device_data.get("rationale") + entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SENSOR, {}) + devs = [] - if entities is not None: + if entities: for entity_key, config in entities.items(): - devs.append(MideaSensorEntity(device, manufacturer, rationale, entity_key, config)) + devs.append(MideaSensorEntity( + coordinator, device, manufacturer, rationale, entity_key, config + )) async_add_entities(devs) class MideaSensorEntity(MideaEntity, SensorEntity): + """Midea sensor entity.""" + + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + ) + self._device = device + self._manufacturer = manufacturer + self._rationale = rationale + self._entity_key = entity_key + self._config = config + + @property + def entity_id_suffix(self) -> str: + """Return the suffix for entity ID.""" + return f"sensor_{self._entity_key}" @property def native_value(self): - return self._device.get_attribute(self._entity_key) + """Return the native value of the sensor.""" + return self.device_attributes.get(self._entity_key) diff --git a/custom_components/midea_auto_codec/switch.py b/custom_components/midea_auto_codec/switch.py index 850a222..1ecb04f 100644 --- a/custom_components/midea_auto_codec/switch.py +++ b/custom_components/midea_auto_codec/switch.py @@ -2,33 +2,78 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( Platform, CONF_DEVICE_ID, - CONF_DEVICE, - CONF_ENTITIES, + CONF_ENTITIES, CONF_DEVICE, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + from .const import ( DOMAIN, DEVICES ) -from .midea_entities import MideaBinaryBaseEntity +from .midea_entity import MideaEntity -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switch entities for Midea devices.""" device_id = config_entry.data.get(CONF_DEVICE_ID) - device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) - manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") - rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") - entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SWITCH) + device_data = hass.data[DOMAIN][DEVICES][device_id] + coordinator = device_data.get("coordinator") + device = device_data.get(CONF_DEVICE) + manufacturer = device_data.get("manufacturer") + rationale = device_data.get("rationale") + entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SWITCH, {}) + devs = [] - if entities is not None: + if entities: for entity_key, config in entities.items(): - devs.append(MideaSwitchEntity(device, manufacturer, rationale, entity_key, config)) + devs.append(MideaSwitchEntity( + coordinator, device, manufacturer, rationale, entity_key, config + )) async_add_entities(devs) -class MideaSwitchEntity(MideaBinaryBaseEntity, SwitchEntity): +class MideaSwitchEntity(MideaEntity, SwitchEntity): + """Midea switch entity.""" - def turn_on(self): - self._set_status_on_off(self._entity_key, True) + def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config): + super().__init__( + coordinator, + device.device_id, + device.device_name, + f"T0x{device.device_type:02X}", + device.sn, + device.sn8, + device.model, + ) + self._device = device + self._manufacturer = manufacturer + self._rationale = rationale + self._entity_key = entity_key + self._config = config - def turn_off(self): - self._set_status_on_off(self._entity_key, False) + @property + def entity_id_suffix(self) -> str: + """Return the suffix for entity ID.""" + return f"switch_{self._entity_key}" + + @property + def is_on(self) -> bool: + """Return if the switch is on.""" + value = self.device_attributes.get(self._entity_key) + if isinstance(value, bool): + return value + return value == 1 or value == "on" or value == "true" + + async def async_turn_on(self): + """Turn the switch on.""" + await self.async_set_attribute(self._entity_key, True) + + async def async_turn_off(self): + """Turn the switch off.""" + await self.async_set_attribute(self._entity_key, False)