forked from HomeAssistant/midea-meiju-codec
Integrate to homeassistant
This commit is contained in:
@@ -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))
|
||||||
|
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -86,10 +86,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
account=account,
|
account=account,
|
||||||
password=password
|
password=password
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
if await self._cloud.login():
|
if await self._cloud.login():
|
||||||
return await self.async_step_home()
|
return await self.async_step_home()
|
||||||
else:
|
else:
|
||||||
return await self.async_step_user(error="account_invalid")
|
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,6 +102,7 @@ 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]
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
if await self._cloud.login():
|
if await self._cloud.login():
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"{user_input[CONF_ACCOUNT]}",
|
title=f"{user_input[CONF_ACCOUNT]}",
|
||||||
@@ -110,6 +115,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
self._cloud = None
|
self._cloud = None
|
||||||
return await self.async_step_user(error="login_failed")
|
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(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
|
@@ -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):
|
||||||
|
127
custom_components/midea_auto_codec/data_coordinator.py
Normal file
127
custom_components/midea_auto_codec/data_coordinator.py
Normal 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
|
104
custom_components/midea_auto_codec/midea_entity.py
Normal file
104
custom_components/midea_auto_codec/midea_entity.py
Normal 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)
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user