19 Commits

Author SHA1 Message Date
sususweet
6166ebf4f7 feat: version 0.1.16 2025-10-31 23:25:56 +08:00
sususweet
39cc28b4dd fix: lua base library location error. 2025-10-31 23:21:39 +08:00
sususweet
e4b939780f feat: update support for device T0x15. 2025-10-31 23:01:09 +08:00
Yingqi Tang
c12e95f2a1 Fix duplicate import of 'os' in lua_runtime.py
Removed duplicate import of 'os' in lua_runtime.py
2025-10-31 22:56:07 +08:00
Yingqi Tang
6129248fed Merge pull request #41 from happyhaha1/master
feat(lua): enhance runtime environment with resilient module loading
2025-10-31 22:54:17 +08:00
Yingqi Tang
3b2d817dd6 Merge branch 'master' into master 2025-10-31 22:54:04 +08:00
sususweet
e9f8f95826 feat: add support for device T0x15. 2025-10-31 22:47:43 +08:00
sususweet
7a28c62ac5 fix: fix lua library error. 2025-10-31 22:46:52 +08:00
sususweet
f2735fd729 feat: add new entity include button and number. 2025-10-31 22:46:29 +08:00
happyhaha1
05e30ab414 feat(lua): enhance runtime environment with resilient module loading
Introduce robust Lua module deployment strategy that ensures proper library
availability across different system configurations. The implementation now
prioritizes Home Assistant's configuration directory for persistent storage,
with automatic failover to temporary locations when permission restrictions
occur. Additionally, the runtime now validates module dependencies during
initialization and provides clear diagnostic warnings for any loading failures.
2025-10-31 10:44:42 +08:00
Yingqi Tang
af6f2d1789 Update README.md 2025-10-28 22:58:02 +08:00
sususweet
665763bb22 feat: update readme 2025-10-28 22:53:26 +08:00
sususweet
aa553cd3a8 feat: version v0.1.14 2025-10-28 22:47:39 +08:00
sususweet
7ee22880ab feat: add support for T0xFA. 2025-10-28 22:44:58 +08:00
sususweet
e941dfc547 Merge remote-tracking branch 'origin/master' 2025-10-28 22:04:45 +08:00
sususweet
059cf3aebf feat: add device support for T0xAC(106J6363). 2025-10-28 22:04:27 +08:00
Yingqi Tang
48fac5ec42 Refactor device mapping for default configuration. 2025-10-28 16:16:40 +08:00
sususweet
c503b14d33 feat: add device support for T0x3D. 2025-10-27 23:05:30 +08:00
sususweet
171d76ee3e feat: add device support for T0x21 switch. 2025-10-27 22:38:12 +08:00
19 changed files with 737 additions and 170 deletions

View File

@@ -3,7 +3,7 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration)
[![Stable](https://img.shields.io/github/v/release/sususweet/midea-meiju-codec)](https://github.com/sususweet/midea-meiju-codec/releases/latest)
English | [简体中文](README_zh_CN.md)
English | [简体中文](README_hans.md)
Get devices from MSmartHome/Midea Meiju homes through the network and control them via Midea's cloud API.
@@ -19,8 +19,10 @@ 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
- T0xA1 Dehumidifier
- T0xAC Air Conditioner
- T0xB2 Electric Steamer
@@ -71,4 +73,4 @@ The example configuration `22012227` demonstrates how to map device attributes t
## Acknowledgments
Thanks to the [midea-meiju-codec](https://github.com/MattedBroadSky/midea-meiju-codec) project for providing prior knowledge.
Thanks to the [midea-meiju-codec](https://github.com/MattedBroadSky/midea-meiju-codec) project for providing prior knowledge.

View File

@@ -19,8 +19,10 @@
## 目前支持的设备类型
- T0x13 电灯
- T0x15 养生壶
- T0x21 中央空调网关
- T0x26 浴霸
- T0x26 浴霸
- T0x3D 电热水瓶
- T0xA1 除湿机
- T0xAC 空调
- T0xB2 电蒸箱

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,20 +140,51 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
async def async_setup(hass: HomeAssistant, config: ConfigType):
hass.data.setdefault(DOMAIN, {})
cjson = os.getcwd() + "/cjson.lua"
bit = os.getcwd() + "/bit.lua"
# if not os.path.exists(cjson):
from .const import CJSON_LUA
cjson_lua = base64.b64decode(CJSON_LUA.encode("utf-8")).decode("utf-8")
with open(cjson, "wt") as fp:
fp.write(cjson_lua)
# if not os.path.exists(bit):
from .const import BIT_LUA
bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8")
with open(bit, "wt") as fp:
fp.write(bit_lua)
return True
# 使用Home Assistant配置目录而不是当前工作目录
config_dir = hass.config.path(DOMAIN)
os.makedirs(config_dir, exist_ok=True)
os.makedirs(hass.config.path(STORAGE_PATH), exist_ok=True)
lua_path = hass.config.path(STORAGE_PATH)
cjson = os.path.join(lua_path, "cjson.lua")
bit = os.path.join(lua_path, "bit.lua")
# 只有文件不存在时才创建
if not os.path.exists(cjson):
from .const import CJSON_LUA
cjson_lua = base64.b64decode(CJSON_LUA.encode("utf-8")).decode("utf-8")
try:
with open(cjson, "wt") as fp:
fp.write(cjson_lua)
except PermissionError as e:
MideaLogger.error(f"Failed to create cjson.lua at {cjson}: {e}")
# 如果无法创建文件,尝试使用临时目录
import tempfile
temp_dir = tempfile.gettempdir()
cjson = os.path.join(temp_dir, "cjson.lua")
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")
try:
with open(bit, "wt") as fp:
fp.write(bit_lua)
except PermissionError as e:
MideaLogger.error(f"Failed to create bit.lua at {bit}: {e}")
# 如果无法创建文件,尝试使用临时目录
import tempfile
temp_dir = tempfile.gettempdir()
bit = os.path.join(temp_dir, "bit.lua")
with open(bit, "wt") as fp:
fp.write(bit_lua)
MideaLogger.warning(f"Using temporary file for bit.lua: {bit}")
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

@@ -63,7 +63,7 @@ class MideaCloud:
def _make_general_data(self):
return {}
async def _api_request(self, endpoint: str, data: dict, header=None) -> dict | None:
async def _api_request(self, endpoint: str, data: dict, header=None, method="POST") -> dict | None:
header = header or {}
if not data.get("reqId"):
data.update({
@@ -91,15 +91,18 @@ class MideaCloud:
_LOGGER.debug(f"Midea cloud API header: {header}")
_LOGGER.debug(f"Midea cloud API dump_data: {dump_data}")
try:
r = await self._session.request("POST", url, headers=header, data=dump_data, timeout=5)
r = await self._session.request(method, url, headers=header, data=dump_data, timeout=5)
raw = await r.read()
_LOGGER.debug(f"Midea cloud API url: {url}, data: {data}, response: {raw}")
response = json.loads(raw)
except Exception as e:
_LOGGER.debug(f"API request attempt failed: {e}")
if int(response["code"]) == 0 and "data" in response:
return response["data"]
if int(response["code"]) == 0:
if "data" in response:
return response["data"]
else:
return {"message": "ok"}
return None
@@ -207,6 +210,10 @@ class MideaCloud:
"""Get status of central AC devices. Subclasses should implement if supported."""
raise NotImplementedError()
async def send_switch_control(self, device_id: str, nodeid: str, switch_control: dict) -> bool:
"""Send control to switch device. Subclasses should implement if supported."""
raise NotImplementedError()
class MeijuCloud(MideaCloud):
APP_ID = "900"
@@ -405,6 +412,39 @@ class MeijuCloud(MideaCloud):
)
return response
async def send_switch_control(self, device_id: str, nodeid: str, switch_control: dict) -> bool:
"""Send control to switch device using the controlPanelFour API with PUT method."""
import uuid
# switch_control 格式: {"endPoint": 1, "attribute": 0}
end_point = switch_control.get("endPoint", 1)
attribute = switch_control.get("attribute", 0)
# 构建请求数据
request_data = {
"msgId": str(uuid.uuid4()).replace("-", ""),
"deviceControlList": [{
"endPoint": end_point,
"attribute": attribute
}],
"deviceId": device_id,
"nodeId": nodeid
}
MideaLogger.debug(f"Sending switch control to device {device_id}: {request_data}")
# 使用PUT方法发送到开关控制API
if response := await self._api_request(
endpoint="/v1/appliance/operation/controlPanelFour/" + device_id,
data=request_data,
method="PUT"
):
MideaLogger.debug(f"[{device_id}] Switch control response: {response}")
return True
else:
MideaLogger.warning(f"[{device_id}] Switch control failed: {response}")
return False
async def download_lua(
self, path: str,
device_type: int,

View File

@@ -1,5 +1,5 @@
import os
import traceback
import lupa
import threading
import json
@@ -9,6 +9,23 @@ 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"')
except Exception as e:
MideaLogger.warning(f"Failed to load cjson: {e}")
try:
self._runtimes.execute('require "bit"')
except Exception as e:
MideaLogger.warning(f"Failed to load bit: {e}")
# 加载设备特定的Lua文件
string = f'dofile("{file}")'
self._runtimes.execute(string)
self._lock = threading.Lock()

View File

@@ -123,9 +123,7 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
for appliance in status_data["appliances"]:
if appliance.get("type") == "0x21" and "extraData" in appliance:
extra_data = appliance["extraData"]
if "attr" in extra_data and "state" in extra_data["attr"]:
state = extra_data["attr"]["state"]
if "attr" in extra_data:
if "nodeid" in extra_data["attr"]:
self.device._attributes["nodeid"] = extra_data["attr"]["nodeid"]
if "masterId" in extra_data["attr"]:
@@ -135,7 +133,8 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
if "idType" in extra_data["attr"]:
self.device._attributes["idType"] = extra_data["attr"]["idType"]
if "condition_attribute" in state:
if "state" in extra_data["attr"] and "condition_attribute" in extra_data["attr"]["state"]:
state = extra_data["attr"]["state"]
condition = state["condition_attribute"]
# 将状态数据更新到设备属性中
for key, value in condition.items():
@@ -153,6 +152,32 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
self.device._attributes[key] = value
else:
self.device._attributes[key] = value
if "endlist" in extra_data["attr"]:
endlist = extra_data["attr"]["endlist"]
# endlist是一个数组包含多个endpoint对象
if isinstance(endlist, list):
for endpoint in endlist:
if "event" in endpoint:
event = endpoint["event"]
endpoint_id = endpoint.get("endpoint", 1)
endpoint_name = endpoint.get("name", f"按键{endpoint_id}")
# 为每个endpoint创建独立的状态属性
for key, value in event.items():
# 创建带endpoint标识的属性名
attr_key = f"endpoint_{endpoint_id}_{key}"
attr_name_key = f"endpoint_{endpoint_id}_name"
# 保存endpoint名称
self.device._attributes[attr_name_key] = endpoint_name
self.device._attributes[attr_key] = value
# 同时保持原有的属性名(用于兼容性)
for key, value in event.items():
# 尝试将数字字符串转换为数字
self.device._attributes[key] = value
break
except Exception as e:
MideaLogger.debug(f"Error polling central AC state: {e}")
@@ -223,6 +248,78 @@ class MideaDataUpdateCoordinator(DataUpdateCoordinator[MideaDeviceData]):
MideaLogger.debug(f"Error sending control to {self.device.device_name}: {e}")
return False
async def async_send_switch_control(self, control: dict) -> bool:
"""发送开关控制命令subtype为00000000的设备"""
try:
cloud = self._cloud
if cloud and hasattr(cloud, "send_switch_control"):
# 获取设备ID和nodeId
masterid = str(self.device.attributes.get("masterId"))
nodeid = str(self.device.attributes.get("nodeid"))
if not nodeid:
MideaLogger.warning(f"No nodeid found for switch device {self._device_id}")
return False
# 根据控制命令确定endPoint和attribute值
end_point = control.get("endpoint", 1) # 从control中获取endpoint默认1
attribute = 0 # 默认attribute
# 根据control内容设置attribute值
if "run_mode" in control:
if control["run_mode"] == "1":
attribute = 1 # 开启
else:
attribute = 0 # 关闭
# 构建控制数据
switch_control = {
"endPoint": end_point,
"attribute": attribute
}
MideaLogger.debug(f"Sending switch control to {self.device.device_name}: {switch_control}")
success = await cloud.send_switch_control(masterid, nodeid, switch_control)
if success:
# 更新本地状态 - 使用类似poll_central的解析方法
await self._update_switch_status_from_control(control)
self.mute_state_update_for_a_while()
self.async_update_listeners()
return True
else:
MideaLogger.debug(f"Failed to send switch control to {self.device.device_name}")
return False
else:
MideaLogger.debug("Cloud service not available for switch control")
return False
except Exception as e:
MideaLogger.debug(f"Error sending switch control to {self.device.device_name}: {e}")
return False
async def _update_switch_status_from_control(self, control: dict) -> None:
"""根据控制命令更新开关状态参照poll_central的解析方法"""
try:
# 获取endpoint ID
endpoint_id = control.get("endpoint", 1)
run_mode = control.get("run_mode", "0")
# 模拟endlist数据结构来更新状态
# 根据run_mode设置OnOff状态
onoff_value = "1" if run_mode == "1" else "0"
# 更新endpoint特定的状态属性
attr_key = f"endpoint_{endpoint_id}_OnOff"
self.device._attributes[attr_key] = onoff_value
# 同时更新兼容性属性
self.device._attributes["OnOff"] = onoff_value
MideaLogger.debug(f"Updated switch status for endpoint {endpoint_id}: OnOff={onoff_value}")
except Exception as e:
MideaLogger.debug(f"Error updating switch status from control: {e}")
def _build_full_central_ac_control(self, new_control: dict) -> dict:
"""构建完整控制命令"""
full_control = {}

View File

@@ -45,7 +45,7 @@ DEVICE_MAPPING = {
Platform.FAN: {
"fan": {
"power": "fan_power",
"fan_speed": [
"speeds": [
{"fan_speed": "1"},
{"fan_speed": "2"},
{"fan_speed": "3"},

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

@@ -3,6 +3,23 @@ from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
DEVICE_MAPPING = {
"00000000": {
"rationale": ["0", "1"],
"queries": [{}],
"centralized": [],
"entities": {
Platform.SWITCH: {
"endpoint_1_OnOff": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": ['0', '1']
},
"endpoint_2_OnOff": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": ['0', '1']
}
},
}
},
"default": {
"rationale": ["off", "on"],
"queries": [{}],

View File

@@ -0,0 +1,48 @@
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": [],
"entities": {
Platform.SWITCH: {
"work_switch": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": ['cancel', 'work']
}
},
Platform.SELECT: {
"warm_target_temp": {
"options": {
"45℃": {"warm_target_temp": "45"},
"55℃": {"warm_target_temp": "55"},
"65℃": {"warm_target_temp": "65"},
"75℃": {"warm_target_temp": "75"},
"85℃": {"warm_target_temp": "85"}
}
},
"boil_target_temp": {
"options": {
"45℃": {"boil_target_temp": "45"},
"55℃": {"boil_target_temp": "55"},
"65℃": {"boil_target_temp": "65"},
"75℃": {"boil_target_temp": "75"},
"85℃": {"boil_target_temp": "85"}
}
}
},
Platform.SENSOR: {
"cur_temp": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES, PRECISION_WHOLE
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
# from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
@@ -132,6 +132,32 @@ DEVICE_MAPPING = {
}
}
},
"106J6363": {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": [],
"entities": {
Platform.CLIMATE: {
"thermostat": {
"power": "water_model_power",
"hvac_modes": {
"off": {"water_model_power": "off"},
"heat": {"water_model_power": "on", "water_model_temperature_auto": "off"},
"auto": {"water_model_power": "on", "water_model_temperature_auto": "on"},
},
"preset_modes": {
"none": {"water_model_go_out": "off"},
"go out": {"water_model_go_out": "on"},
},
"target_temperature": "water_model_temperature_set",
"min_temp": 25,
"max_temp": 60,
"temperature_unit": UnitOfTemperature.CELSIUS,
"precision": PRECISION_WHOLE,
}
},
}
},
"26093139": {
"rationale": [0, 3],
"queries": [{}, {"query_type": "run_status"}],

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

@@ -8,34 +8,38 @@ DEVICE_MAPPING = {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": [
"power", "humidify", "swing", "anion", "display_on_off",
"dust_reset", "temp_wind_switch", "filter_reset"
"power", "swing", "display_on_off", "temp_wind_switch",
],
"entities": {
Platform.BINARY_SENSOR: {
"power": {
"device_class": BinarySensorDeviceClass.POWER,
},
"humidify": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"swing": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"anion": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
Platform.SWITCH: {
"display_on_off": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
"dust_reset": {
"device_class": BinarySensorDeviceClass.RUNNING,
"device_class": SwitchDeviceClass.SWITCH,
"rationale": ["on", "off"]
},
"temp_wind_switch": {
"device_class": BinarySensorDeviceClass.RUNNING,
"device_class": SwitchDeviceClass.SWITCH,
},
"filter_reset": {
"device_class": BinarySensorDeviceClass.RUNNING,
},
Platform.FAN: {
"fan": {
"power": "power",
"speeds": [
{"gear": "1"},
{"gear": "2"},
{"gear": "3"},
{"gear": "4"},
{"gear": "5"},
{"gear": "6"},
{"gear": "7"},
{"gear": "8"},
{"gear": "9"},
],
"oscillate": "swing",
"preset_modes": {
"normal": {"mode": "normal"},
"sleep": {"mode": "sleep"},
"baby": {"mode": "baby"}
}
}
},
Platform.SELECT: {
@@ -65,16 +69,6 @@ DEVICE_MAPPING = {
"both": {"swing_direction": "both"}
}
},
"scene": {
"options": {
"none": {"scene": "none"},
"auto": {"scene": "auto"},
"sleep": {"scene": "sleep"},
"work": {"scene": "work"},
"study": {"scene": "study"},
"party": {"scene": "party"}
}
},
"sleep_sensor": {
"options": {
"none": {"sleep_sensor": "none"},
@@ -83,27 +77,6 @@ DEVICE_MAPPING = {
"both": {"sleep_sensor": "both"}
}
},
"mode": {
"options": {
"normal": {"mode": "normal"},
"auto": {"mode": "auto"},
"manual": {"mode": "manual"},
"sleep": {"mode": "sleep"},
"turbo": {"mode": "turbo"},
"quiet": {"mode": "quiet"}
}
},
"gear": {
"options": {
"1": {"gear": "1"},
"2": {"gear": "2"},
"3": {"gear": "3"},
"4": {"gear": "4"},
"5": {"gear": "5"},
"6": {"gear": "6"},
"auto": {"gear": "auto"}
}
}
},
Platform.SENSOR: {
"real_gear": {
@@ -120,19 +93,6 @@ DEVICE_MAPPING = {
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"battery_status": {
"device_class": SensorDeviceClass.BATTERY,
"state_class": SensorStateClass.MEASUREMENT
},
"battery_level": {
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT
},
"error_code": {
"device_class": SensorDeviceClass.ENUM,
"state_class": SensorStateClass.MEASUREMENT
},
"temperature_feedback": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
@@ -162,10 +122,6 @@ DEVICE_MAPPING = {
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"version": {
"device_class": SensorDeviceClass.ENUM,
"state_class": SensorStateClass.MEASUREMENT
},
"pm25": {
"device_class": SensorDeviceClass.PM25,
"unit_of_measurement": "µg/m³",
@@ -176,22 +132,22 @@ DEVICE_MAPPING = {
"state_class": SensorStateClass.MEASUREMENT
},
"lr_diy_down_percent": {
"device_class": SensorDeviceClass.ENUM,
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT
},
"lr_diy_up_percent": {
"device_class": SensorDeviceClass.ENUM,
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT
},
"ud_diy_down_percent": {
"device_class": SensorDeviceClass.ENUM,
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT
},
"ud_diy_up_percent": {
"device_class": SensorDeviceClass.ENUM,
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT
}

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"issue_tracker": "https://github.com/sususweet/midea-meiju-codec/issues",
"requirements": ["lupa>=2.0"],
"version": "v0.1.10"
"version": "v0.1.16"
}

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

View File

@@ -5,6 +5,7 @@ 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
@@ -43,6 +44,9 @@ class MideaSwitchEntity(MideaEntity, SwitchEntity):
"""Midea switch entity."""
def __init__(self, coordinator, device, manufacturer, rationale, entity_key, config):
# 自动判断是否为中央空调设备T0x21
self._is_central_ac = device.device_type == 0x21
super().__init__(
coordinator,
device.device_id,
@@ -67,12 +71,37 @@ class MideaSwitchEntity(MideaEntity, SwitchEntity):
async def async_turn_on(self):
"""Turn the switch on."""
# Use attribute from config if available, otherwise fall back to entity_key
attribute = self._config.get("attribute", self._entity_key)
await self._async_set_status_on_off(attribute, True)
if self._is_central_ac:
await self._async_set_central_ac_switch_status(True)
else:
await self._async_set_status_on_off(attribute, True)
async def async_turn_off(self):
"""Turn the switch off."""
# Use attribute from config if available, otherwise fall back to entity_key
attribute = self._config.get("attribute", self._entity_key)
await self._async_set_status_on_off(attribute, False)
if self._is_central_ac:
await self._async_set_central_ac_switch_status(False)
else:
await self._async_set_status_on_off(attribute, False)
async def _async_set_central_ac_switch_status(self, is_on: bool):
"""设置中央空调开关设备的状态"""
# 从entity_key中提取endpoint ID
# entity_key格式: endpoint_1_OnOff -> 提取出 1
endpoint_id = 1 # 默认值
if self._entity_key.startswith("endpoint_"):
try:
# 提取endpoint_后面的数字
parts = self._entity_key.split("_")
if len(parts) >= 2:
endpoint_id = int(parts[1])
except (ValueError, IndexError):
MideaLogger.warning(f"Failed to extract endpoint ID from {self._entity_key}, using default 1")
# 构建控制命令
control = {
"run_mode": "1" if is_on else "0",
"endpoint": endpoint_id
}
await self.coordinator.async_send_switch_control(control)

View File

@@ -291,30 +291,6 @@
"execute": {
"name": "Execute"
},
"power": {
"name": "Power"
},
"humidify": {
"name": "Humidify"
},
"swing": {
"name": "Swing"
},
"anion": {
"name": "Anion"
},
"display_on_off": {
"name": "Display On/Off"
},
"dust_reset": {
"name": "Dust Reset"
},
"temp_wind_switch": {
"name": "Temp Wind Switch"
},
"filter_reset": {
"name": "Filter Reset"
},
"heat_status": {
"name": "Heat Status"
},
@@ -373,6 +349,12 @@
}
},
"select": {
"warm_target_temp": {
"name": "Warm Target Temperature"
},
"boil_target_temp": {
"name": "Boil Target Temperature"
},
"add_rinse": {
"name": "Add Rinse"
},
@@ -1533,6 +1515,9 @@
}
},
"switch": {
"temp_wind_switch": {
"name": "Wind Change with Temperature"
},
"screen_close": {
"name": "Screen Close"
},
@@ -2387,6 +2372,33 @@
},
"is_lock_rc": {
"name": "Remote Control Lock"
},
"endpoint_1_onoff": {
"name": "Button 1"
},
"endpoint_2_onoff": {
"name": "Button 2"
},
"endpoint_3_onoff": {
"name": "Button 3"
},
"endpoint_4_onoff": {
"name": "Button 4"
},
"endpoint_5_onoff": {
"name": "Button 5"
},
"endpoint_6_onoff": {
"name": "Button 6"
},
"endpoint_7_onoff": {
"name": "Button 7"
},
"endpoint_8_onoff": {
"name": "Button 8"
},
"work_switch": {
"name": "Work Switch"
}
}
}

View File

@@ -291,30 +291,6 @@
"execute": {
"name": "执行"
},
"power": {
"name": "电源"
},
"humidify": {
"name": "加湿"
},
"swing": {
"name": "摆风"
},
"anion": {
"name": "负离子"
},
"display_on_off": {
"name": "显示开关"
},
"dust_reset": {
"name": "灰尘重置"
},
"temp_wind_switch": {
"name": "温风开关"
},
"filter_reset": {
"name": "滤网重置"
},
"heat_status": {
"name": "加热状态"
},
@@ -377,6 +353,12 @@
}
},
"select": {
"warm_target_temp": {
"name": "保温目标温度"
},
"boil_target_temp": {
"name": "煮沸目标温度"
},
"add_rinse": {
"name": "加漂洗"
},
@@ -1537,6 +1519,9 @@
}
},
"switch": {
"temp_wind_switch": {
"name": "风随温变"
},
"screen_close": {
"name": "屏幕关闭"
},
@@ -2391,6 +2376,33 @@
},
"is_lock_rc": {
"name": "遥控锁定"
},
"endpoint_1_onoff": {
"name": "按键一"
},
"endpoint_2_onoff": {
"name": "按键二"
},
"endpoint_3_onoff": {
"name": "按键三"
},
"endpoint_4_onoff": {
"name": "按键四"
},
"endpoint_5_onoff": {
"name": "按键五"
},
"endpoint_6_onoff": {
"name": "按键六"
},
"endpoint_7_onoff": {
"name": "按键七"
},
"endpoint_8_onoff": {
"name": "按键八"
},
"work_switch": {
"name": "工作开关"
}
}
}