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
- T0x13 Electric Light
- T0x15 Water Heater
- T0x21 Central Air Conditioning Gateway
- T0x26 Bath Heater
- T0x3D Water Heater

View File

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

View File

@@ -57,7 +57,9 @@ PLATFORMS: list[Platform] = [
Platform.WATER_HEATER,
Platform.FAN,
Platform.LIGHT,
Platform.HUMIDIFIER
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.BUTTON
]
@@ -138,14 +140,14 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
async def async_setup(hass: HomeAssistant, config: ConfigType):
hass.data.setdefault(DOMAIN, {})
# 使用Home Assistant配置目录而不是当前工作目录
config_dir = hass.config.path(DOMAIN)
os.makedirs(config_dir, exist_ok=True)
cjson = os.path.join(config_dir, "cjson.lua")
bit = os.path.join(config_dir, "bit.lua")
# 只有文件不存在时才创建
if not os.path.exists(cjson):
from .const import CJSON_LUA
@@ -162,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
with open(cjson, "wt") as fp:
fp.write(cjson_lua)
MideaLogger.warning(f"Using temporary file for cjson.lua: {cjson}")
if not os.path.exists(bit):
from .const import BIT_LUA
bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8")
@@ -178,9 +180,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
with open(bit, "wt") as fp:
fp.write(bit_lua)
MideaLogger.warning(f"Using temporary file for bit.lua: {bit}")
return True
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
device_type = config_entry.data.get(CONF_TYPE)

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 os
import lupa
@@ -9,11 +10,11 @@ from .logger import MideaLogger
class LuaRuntime:
def __init__(self, file):
self._runtimes = lupa.lua51.LuaRuntime()
# 设置Lua路径包含cjson.lua和bit.lua的目录
lua_dir = os.path.dirname(os.path.abspath(file))
self._runtimes.execute(f'package.path = package.path .. ";{lua_dir}/?.lua"')
# 加载必需的Lua库
try:
self._runtimes.execute('require "cjson"')

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