Merge branch 'master' into master

This commit is contained in:
Yingqi Tang
2025-10-31 22:54:04 +08:00
committed by GitHub
8 changed files with 304 additions and 24 deletions

View File

@@ -19,6 +19,7 @@ Get devices from MSmartHome/Midea Meiju homes through the network and control th
## Currently Supported Device Types ## Currently Supported Device Types
- T0x13 Electric Light - T0x13 Electric Light
- T0x15 Water Heater
- T0x21 Central Air Conditioning Gateway - T0x21 Central Air Conditioning Gateway
- T0x26 Bath Heater - T0x26 Bath Heater
- T0x3D Water Heater - T0x3D Water Heater

View File

@@ -19,6 +19,7 @@
## 目前支持的设备类型 ## 目前支持的设备类型
- T0x13 电灯 - T0x13 电灯
- T0x15 养生壶
- T0x21 中央空调网关 - T0x21 中央空调网关
- T0x26 浴霸 - T0x26 浴霸
- T0x3D 电热水瓶 - T0x3D 电热水瓶

View File

@@ -57,7 +57,9 @@ PLATFORMS: list[Platform] = [
Platform.WATER_HEATER, Platform.WATER_HEATER,
Platform.FAN, Platform.FAN,
Platform.LIGHT, Platform.LIGHT,
Platform.HUMIDIFIER Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.BUTTON
] ]
@@ -181,7 +183,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
return True return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
device_type = config_entry.data.get(CONF_TYPE) device_type = config_entry.data.get(CONF_TYPE)
MideaLogger.debug(f"async_setup_entry type={device_type} data={config_entry.data}") MideaLogger.debug(f"async_setup_entry type={device_type} data={config_entry.data}")

View File

@@ -0,0 +1,99 @@
from homeassistant.components.button import ButtonEntity
from homeassistant.const import Platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .core.logger import MideaLogger
from .midea_entity import MideaEntity
from . import load_device_config
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up button entities for Midea devices."""
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
if not account_bucket:
async_add_entities([])
return
device_list = account_bucket.get("device_list", {})
coordinator_map = account_bucket.get("coordinator_map", {})
devs = []
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.BUTTON, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")
coordinator = coordinator_map.get(device_id)
device = coordinator.device if coordinator else None
for entity_key, ecfg in entities_cfg.items():
devs.append(MideaButtonEntity(
coordinator, device, manufacturer, rationale, entity_key, ecfg
))
async_add_entities(devs)
class MideaButtonEntity(MideaEntity, ButtonEntity):
"""Midea button 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,
entity_key,
device=device,
manufacturer=manufacturer,
rationale=rationale,
config=config,
)
async def async_press(self) -> None:
"""Handle the button press."""
# 从配置中获取要执行的命令或操作
command = self._config.get("command")
attribute = self._config.get("attribute", self._entity_key)
value = self._config.get("value")
# 判断是否为中央空调设备T0x21
is_central_ac = self._device.device_type == 0x21 if self._device else False
if command:
# 如果配置中指定了命令,执行该命令
if isinstance(command, dict):
# 如果是字典,可能需要发送多个属性
await self.async_set_attributes(command)
elif isinstance(command, str):
# 如果是字符串,可能是特殊命令类型
await self._async_execute_command(command)
elif value is not None:
# 如果配置中指定了值,设置该属性值
await self.async_set_attribute(attribute, value)
else:
# 默认行为:如果没有指定命令或值,记录警告
MideaLogger.warning(
f"Button {self._entity_key} has no command or value configured"
)
async def _async_execute_command(self, command: str) -> None:
"""Execute a special command."""
# 这里可以处理特殊的命令类型
# 例如:重启、重置、测试等
if command == "reset" or command == "restart":
# 可以在这里实现重置或重启逻辑
MideaLogger.debug(f"Executing {command} command for button {self._entity_key}")
else:
# 对于其他命令,可以通过 coordinator 发送
await self.coordinator.async_send_command(0, command)

View File

@@ -1,3 +1,4 @@
import os
import traceback import traceback
import os import os
import lupa import lupa

View File

@@ -0,0 +1,60 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfVolume, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \
UnitOfEnergy, UnitOfPower, PRECISION_WHOLE
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
DEVICE_MAPPING = {
"default": {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": ["warm_target_temp", "boil_target_temp", "meate_select", "max_work_time", "warm_time_min"],
"entities": {
Platform.BINARY_SENSOR: {
"islack_water": {
"device_class": BinarySensorDeviceClass.PROBLEM,
}
},
# Platform.NUMBER: {
# "warm_time_min": {
# "min": 0,
# "max": 480,
# "step": 60
# },
# "max_work_time": {
# "min": 0,
# "max": 12,
# "step": 1
# },
# "warm_target_temp": {
# "min": 0,
# "max": 100,
# "step": 1
# },
# "boil_target_temp": {
# "min": 0,
# "max": 100,
# "step": 1
# },
# },
Platform.SELECT: {
"work_mode": {
"options": {
"取消": {"work_mode": "0", "work_switch": "cancel"},
"烧水": {"work_mode": "1", "work_switch": "start"},
"除氯": {"work_mode": "2", "work_switch": "start"},
"花草茶": {"work_mode": "4", "work_switch": "start"},
"养生汤": {"work_mode": "5", "work_switch": "start"},
}
}
},
Platform.SENSOR: {
"current_temp": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
}
}
}
}
}

View File

@@ -66,30 +66,30 @@ DEVICE_MAPPING = {
Platform.SELECT: { Platform.SELECT: {
"mode": { "mode": {
"options": { "options": {
"Rice": {"mode": "essence_rice", "work_status": "cooking"}, "精华饭": {"mode": "essence_rice", "work_status": "cooking"},
"Porridge": {"mode": "gruel", "work_status": "cooking"}, "稀饭": {"mode": "gruel", "work_status": "cooking"},
"热饭": {"mode": "heat_rice", "work_status": "cooking"}, "热饭": {"mode": "heat_rice", "work_status": "cooking"},
"Congee": {"mode": "boil_congee", "work_status": "cooking"}, "煮粥": {"mode": "boil_congee", "work_status": "cooking"},
"Soup": {"mode": "cook_soup", "work_status": "cooking"}, "煲汤": {"mode": "cook_soup", "work_status": "cooking"},
"Steam": {"mode": "stewing", "work_status": "cooking"}, "蒸煮": {"mode": "stewing", "work_status": "cooking"},
} }
}, },
"rice_type": { "rice_type": {
"options": { "options": {
"None": {"rice_type": "none"}, "": {"rice_type": "none"},
"Northeast rice": {"rice_type": "northeast"}, "东北大米": {"rice_type": "northeast"},
"Long-grain rice": {"rice_type": "longrain"}, "长粒米": {"rice_type": "longrain"},
"Fragrant rice": {"rice_type": "fragrant"}, "香米": {"rice_type": "fragrant"},
"Wuchang rice": {"rice_type": "five"}, "五常大米": {"rice_type": "five"},
} }
}, },
"work_status": { "work_status": {
"options": { "options": {
"Stop": {"work_status": "cancel"}, "停止": {"work_status": "cancel"},
"Cooking": {"work_status": "cooking"}, "烹饪": {"work_status": "cooking"},
"Warming": {"work_status": "keep_warm"}, "保温": {"work_status": "keep_warm"},
"Soaking": {"work_status": "awakening_rice"}, "醒米": {"work_status": "awakening_rice"},
"Delay": {"work_status": "schedule"} "预约": {"work_status": "schedule"}
} }
} }
} }

View File

@@ -0,0 +1,117 @@
from homeassistant.components.number import NumberEntity
from homeassistant.const import Platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .core.logger import MideaLogger
from .midea_entity import MideaEntity
from . import load_device_config
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up number entities for Midea devices."""
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
if not account_bucket:
async_add_entities([])
return
device_list = account_bucket.get("device_list", {})
coordinator_map = account_bucket.get("coordinator_map", {})
devs = []
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.NUMBER, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")
coordinator = coordinator_map.get(device_id)
device = coordinator.device if coordinator else None
for entity_key, ecfg in entities_cfg.items():
devs.append(MideaNumberEntity(
coordinator, device, manufacturer, rationale, entity_key, ecfg
))
async_add_entities(devs)
class MideaNumberEntity(MideaEntity, NumberEntity):
"""Midea number 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,
entity_key,
device=device,
manufacturer=manufacturer,
rationale=rationale,
config=config,
)
# 从配置中读取数值范围,如果没有则使用默认值
self._min_value = self._config.get("min", 0.0)
self._max_value = self._config.get("max", 100.0)
self._step = self._config.get("step", 1.0)
self._mode = self._config.get("mode", "auto") # auto, box, slider
@property
def native_value(self) -> float | None:
"""Return the current value."""
# Use attribute from config if available, otherwise fall back to entity_key
attribute = self._config.get("attribute", self._entity_key)
value = self._get_nested_value(attribute)
if value is None:
return None
# 确保返回的是数值类型
try:
return float(value)
except (ValueError, TypeError):
MideaLogger.warning(
f"Failed to convert value '{value}' to float for number entity {self._entity_key}"
)
return None
@property
def native_min_value(self) -> float:
"""Return the minimum value."""
return float(self._min_value)
@property
def native_max_value(self) -> float:
"""Return the maximum value."""
return float(self._max_value)
@property
def native_step(self) -> float:
"""Return the step value."""
return float(self._step)
@property
def mode(self) -> str:
"""Return the mode of the number entity."""
return self._mode
async def async_set_native_value(self, value: float) -> None:
"""Set the value of the number entity."""
# 确保值在有效范围内
value = max(self._min_value, min(self._max_value, value))
# Use attribute from config if available, otherwise fall back to entity_key
attribute = self._config.get("attribute", self._entity_key)
# 如果配置中指定了转换函数或映射,可以在这里处理
# 否则直接设置属性值
await self.async_set_attribute(attribute, str(int(value)))