Integrate to homeassistant

This commit is contained in:
sususweet
2025-09-09 23:52:48 +08:00
parent d9cb062467
commit 2e74178de8
9 changed files with 567 additions and 119 deletions

View File

@@ -28,6 +28,7 @@ from homeassistant.const import (
) )
from .core.logger import MideaLogger from .core.logger import MideaLogger
from .core.device import MiedaDevice from .core.device import MiedaDevice
from .data_coordinator import MideaDataUpdateCoordinator
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES, DEVICES,
@@ -98,29 +99,24 @@ def register_services(hass: HomeAssistant):
attributes = service.data.get("attributes") attributes = service.data.get("attributes")
MideaLogger.debug(f"Service called: set_attributes, device_id: {device_id}, attributes: {attributes}") MideaLogger.debug(f"Service called: set_attributes, device_id: {device_id}, attributes: {attributes}")
try: try:
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator")
except KeyError: 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 set_attributes: the device {device_id} isn't exist.")
return return
if device: if coordinator:
device.set_attributes(attributes) await coordinator.async_set_attributes(attributes)
async def async_send_command(service: ServiceCall): async def async_send_command(service: ServiceCall):
device_id = service.data.get("device_id") device_id = service.data.get("device_id")
cmd_type = service.data.get("cmd_type") cmd_type = service.data.get("cmd_type")
cmd_body = service.data.get("cmd_body") cmd_body = service.data.get("cmd_body")
try: try:
cmd_body = bytearray.fromhex(cmd_body) coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator")
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)
except KeyError: 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 return
if device: if coordinator:
device.send_command(cmd_type, cmd_body) await coordinator.async_send_command(cmd_type, cmd_body)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@@ -222,9 +218,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
if DEVICES not in hass.data[DOMAIN]: if DEVICES not in hass.data[DOMAIN]:
hass.data[DOMAIN][DEVICES] = {} 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] = {}
hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device 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] = {} hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {}
config = load_device_config(hass, device_type, sn8) config = load_device_config(hass, device_type, sn8)
if config is not None and len(config) > 0: if config is not None and len(config) > 0:
queries = config.get("queries") 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]["manufacturer"] = config.get("manufacturer")
hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale") hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale")
hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES) hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES)
for platform in ALL_PLATFORM: for platform in ALL_PLATFORM:
hass.async_create_task(hass.config_entries.async_forward_entry_setup( hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, platform)) config_entry, platform))

View File

@@ -5,58 +5,115 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import ( from homeassistant.const import (
Platform, Platform,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICE, CONF_ENTITIES, CONF_DEVICE
CONF_ENTITIES
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES 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_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) device_data = hass.data[DOMAIN][DEVICES][device_id]
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") coordinator = device_data.get("coordinator")
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") device = device_data.get(CONF_DEVICE)
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.BINARY_SENSOR) manufacturer = device_data.get("manufacturer")
devs = [MideaDeviceStatusSensorEntity(device, manufacturer, rationale,"Status", {})] rationale = device_data.get("rationale")
if entities is not None: 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(): 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) 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 @property
def device_class(self): def device_class(self):
"""Return the device class."""
return BinarySensorDeviceClass.CONNECTIVITY return BinarySensorDeviceClass.CONNECTIVITY
@property @property
def icon(self): def icon(self):
"""Return the icon."""
return "mdi:devices" return "mdi:devices"
@property @property
def is_on(self): def is_on(self):
return self._device.connected """Return if the device is connected."""
return self.coordinator.data.connected
@property
def available(self):
return True
@property @property
def extra_state_attributes(self) -> dict: def extra_state_attributes(self) -> dict:
return self._device.attributes """Return extra state attributes."""
return self.device_attributes
def update_state(self, status):
try:
self.schedule_update_ha_state()
except Exception as e:
pass
class MideaBinarySensorEntity(MideaBinaryBaseEntity, BinarySensorEntity): class MideaBinarySensorEntity(MideaEntity, BinarySensorEntity):
pass """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"

View File

@@ -8,33 +8,59 @@ from homeassistant.const import (
Platform, Platform,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_ENTITIES, CONF_ENTITIES,
CONF_DEVICE, ATTR_TEMPERATURE, CONF_DEVICE
ATTR_TEMPERATURE
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES 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_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) device_data = hass.data[DOMAIN][DEVICES][device_id]
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") coordinator = device_data.get("coordinator")
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") device = device_data.get(CONF_DEVICE)
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.CLIMATE) manufacturer = device_data.get("manufacturer")
rationale = device_data.get("rationale")
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.CLIMATE, {})
devs = [] devs = []
if entities is not None: if entities:
for entity_key, config in entities.items(): 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) async_add_entities(devs)
class MideaClimateEntity(MideaEntity, ClimateEntity): class MideaClimateEntity(MideaEntity, ClimateEntity):
def __init__(self, device, manufacturer, rationale, entity_key, config): def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
super().__init__(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_power = self._config.get("power")
self._key_hvac_modes = self._config.get("hvac_modes") self._key_hvac_modes = self._config.get("hvac_modes")
self._key_preset_modes = self._config.get("preset_modes") self._key_preset_modes = self._config.get("preset_modes")
@@ -65,30 +91,30 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
@property @property
def current_temperature(self): def current_temperature(self):
return self._device.get_attribute(self._key_current_temperature) return self.device_attributes.get(self._key_current_temperature)
@property @property
def target_temperature(self): def target_temperature(self):
if isinstance(self._key_target_temperature, list): if isinstance(self._key_target_temperature, list):
temp_int = self._device.get_attribute(self._key_target_temperature[0]) temp_int = self.device_attributes.get(self._key_target_temperature[0])
tem_dec = self._device.get_attribute(self._key_target_temperature[1]) tem_dec = self.device_attributes.get(self._key_target_temperature[1])
if temp_int is not None and tem_dec is not None: if temp_int is not None and tem_dec is not None:
return temp_int + tem_dec return temp_int + tem_dec
return None return None
else: else:
return self._device.get_attribute(self._key_target_temperature) return self.device_attributes.get(self._key_target_temperature)
@property @property
def min_temp(self): def min_temp(self):
if isinstance(self._key_min_temp, str): 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: else:
return float(self._key_min_temp) return float(self._key_min_temp)
@property @property
def max_temp(self): def max_temp(self):
if isinstance(self._key_max_temp, str): 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: else:
return float(self._key_max_temp) return float(self._key_max_temp)
@@ -140,13 +166,13 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
def is_aux_heat(self): def is_aux_heat(self):
return self._get_status_on_off(self._key_aux_heat) return self._get_status_on_off(self._key_aux_heat)
def turn_on(self): async def async_turn_on(self):
self._set_status_on_off(self._key_power, True) await self._async_set_status_on_off(self._key_power, True)
def turn_off(self): async def async_turn_off(self):
self._set_status_on_off(self._key_power, False) 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: if ATTR_TEMPERATURE not in kwargs:
return return
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
@@ -162,32 +188,68 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
new_status[self._key_target_temperature[1]] = temp_dec new_status[self._key_target_temperature[1]] = temp_dec
else: else:
new_status[self._key_target_temperature] = temperature 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) 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) 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) 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) 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: async def async_turn_aux_heat_on(self) -> None:
self._set_status_on_off(self._key_aux_heat, True) await self._async_set_status_on_off(self._key_aux_heat, True)
def turn_aux_heat_off(self) -> None: async def async_turn_aux_heat_off(self) -> None:
self._set_status_on_off(self._key_aux_heat, False) await self._async_set_status_on_off(self._key_aux_heat, False)
def update_state(self, status): def _get_status_on_off(self, key):
try: """Get on/off status from device attributes."""
self.schedule_update_ha_state() if key is None:
except Exception as e: return False
pass 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

View File

@@ -86,10 +86,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
account=account, account=account,
password=password password=password
) )
if await self._cloud.login(): try:
return await self.async_step_home() if await self._cloud.login():
else: return await self.async_step_home()
return await self.async_step_user(error="account_invalid") 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 user_input is not None:
if self._cloud is None: if self._cloud is None:
self._cloud = get_midea_cloud( self._cloud = get_midea_cloud(
@@ -98,16 +102,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
account=user_input[CONF_ACCOUNT], account=user_input[CONF_ACCOUNT],
password=user_input[CONF_PASSWORD] password=user_input[CONF_PASSWORD]
) )
if await self._cloud.login(): try:
return self.async_create_entry( if await self._cloud.login():
title=f"{user_input[CONF_ACCOUNT]}", return self.async_create_entry(
data={ title=f"{user_input[CONF_ACCOUNT]}",
CONF_TYPE: CONF_ACCOUNT, data={
CONF_ACCOUNT: user_input[CONF_ACCOUNT], CONF_TYPE: CONF_ACCOUNT,
CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_ACCOUNT: user_input[CONF_ACCOUNT],
CONF_SERVER: user_input[CONF_SERVER] CONF_PASSWORD: user_input[CONF_PASSWORD],
}) CONF_SERVER: user_input[CONF_SERVER]
else: })
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 self._cloud = None
return await self.async_step_user(error="login_failed") return await self.async_step_user(error="login_failed")
return self.async_show_form( return self.async_show_form(

View File

@@ -326,6 +326,10 @@ class MiedaDevice(threading.Thread):
def _device_connected(self, connected=True): def _device_connected(self, connected=True):
self._connected = connected self._connected = connected
status = {"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) self._update_all(status)
def _update_all(self, status): def _update_all(self, status):

View File

@@ -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

View File

@@ -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)

View File

@@ -2,31 +2,67 @@ from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
Platform, Platform,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICE, CONF_ENTITIES, CONF_DEVICE
CONF_ENTITIES
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES 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_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) device_data = hass.data[DOMAIN][DEVICES][device_id]
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") coordinator = device_data.get("coordinator")
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") device = device_data.get(CONF_DEVICE)
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SENSOR) manufacturer = device_data.get("manufacturer")
rationale = device_data.get("rationale")
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SENSOR, {})
devs = [] devs = []
if entities is not None: if entities:
for entity_key, config in entities.items(): 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) async_add_entities(devs)
class MideaSensorEntity(MideaEntity, SensorEntity): 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 @property
def native_value(self): 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)

View File

@@ -2,33 +2,78 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.const import ( from homeassistant.const import (
Platform, Platform,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICE, CONF_ENTITIES, CONF_DEVICE,
CONF_ENTITIES,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES 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_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE) device_data = hass.data[DOMAIN][DEVICES][device_id]
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer") coordinator = device_data.get("coordinator")
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale") device = device_data.get(CONF_DEVICE)
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SWITCH) manufacturer = device_data.get("manufacturer")
rationale = device_data.get("rationale")
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SWITCH, {})
devs = [] devs = []
if entities is not None: if entities:
for entity_key, config in entities.items(): 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) async_add_entities(devs)
class MideaSwitchEntity(MideaBinaryBaseEntity, SwitchEntity): class MideaSwitchEntity(MideaEntity, SwitchEntity):
"""Midea switch entity."""
def turn_on(self): def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
self._set_status_on_off(self._entity_key, True) 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): @property
self._set_status_on_off(self._entity_key, False) 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)