33 Commits

Author SHA1 Message Date
sususweet
7476d6ad1d feat: add support for T0xCD 2025-11-07 01:08:38 +08:00
sususweet
883369184b feat: fix support for T0xE2 2025-11-06 22:31:08 +08:00
sususweet
4fe689217a feat: add support for washing machine T0xDA 2025-11-05 20:58:00 +08:00
sususweet
a680cd43b8 feat: update device mapping for T0xFA 2025-11-04 16:03:17 +08:00
sususweet
05ad0c3a15 feat: version 0.1.20 2025-11-03 11:38:57 +08:00
sususweet
aea43aad0c feat: change entity for water heater. 2025-11-03 11:38:11 +08:00
sususweet
329fbce3dd feat: add support for T0x9B. Fixed #35. 2025-11-02 16:13:33 +08:00
sususweet
f9a1f57d31 feat: add support for T0x9C. 2025-11-02 16:02:21 +08:00
sususweet
ee6c5b18af feat: update central ac control protocol 2025-11-02 00:37:32 +08:00
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
sususweet
087c5db497 feat: add device support for T0xFB. 2025-10-22 16:01:20 +08:00
sususweet
7327ee3433 feat: update readme. 2025-10-21 16:01:48 +08:00
sususweet
47df52654a Merge pull request #32 from asakiasako/feature/add-colmo-turing-central-AC
feat: Add COLMO Turing Central AC
2025-10-20 09:57:30 +08:00
Setsuna
b2bf61f85c readability optimization 2025-10-18 23:30:26 +08:00
Setsuna
80e646637a feat: Add COLMO Turing Central AC
1. Add device mapping of multiple Turing ACs in T0xAC.py
2. Necessary changes in load_device_config() to support tuple or re pattern as the device map key
3. Update in climate.py to support target humidity feature
4. Add missing translations and icons
2025-10-16 00:27:04 +08:00
31 changed files with 1824 additions and 421 deletions

View File

@@ -1,60 +1,70 @@
# Midea Auto Cloud
[![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_hans.md)
- 自动查找和发现设备
- 自动下载设备的协议文件
- 将设备状态更新为设备可见的属性
Get devices from MSmartHome/Midea Meiju homes through the network and control them via Midea's cloud API.
## 版本说明
- Automatically discover and find devices
- Automatically download device protocol files
- Update device status to visible device attributes
- 所有设备默认可生成一个名为Status的二进制传感器其属性中列出了设备可访问的所有属性当然有些值不可设置
- Status实体前几项列出了该设备的分类信息供参考
## Version Notes
## 目前支持的设备类型
- All devices can generate a binary sensor named "Status" by default, which lists all accessible device attributes in its properties (some values are not settable)
- The first few items of the Status entity list the device classification information for reference
- T0x13 电灯
- T0x21 中央空调网关
- T0x26 浴霸
- T0xA1 除湿机
- T0xAC 空调
- T0xB2 电蒸箱
- T0xB3 消毒碗柜
- T0xB6 抽油烟机
- T0xB7 燃气灶
- T0xB8 智能扫地机器人
- T0xCA 对开门冰箱
- T0xCC 中央空调(风管机)Wi-Fi线控器
- T0xCE 新风机
- T0xCF 中央空调暖家
- T0xD9 复式洗衣机
- T0xDB 滚筒洗衣机
- T0xDC 干衣机
- T0xE1 洗碗机
- T0xE2 电热水器
- T0xE3 恒温式燃气热水器
- T0xEA 电饭锅
- T0xED 软水机
- T0xFA 电风扇
- T0xFD 加湿器
## Currently Supported Device Types
欢迎合作开发添加更多设备支持。
- T0x13 Electric Light
- T0x15 Water Heater
- T0x21 Central Air Conditioning Gateway
- T0x26 Bath Heater
- T0x3D Water Heater
- T0x9B Steam oven
- T0x9C Integrated Gas Stove
- T0xA1 Dehumidifier
- T0xAC Air Conditioner
- T0xB2 Electric Steamer
- T0xB3 Disinfection Cabinet
- T0xB6 Range Hood
- T0xB7 Gas Stove
- T0xB8 Smart Robot Vacuum
- T0xCA French Door Refrigerator
- T0xCC Central Air Conditioning (Ducted) Wi-Fi Controller
- T0xCD Air Energy Water Heater
- T0xCE Fresh Air System
- T0xCF Central Air Conditioning Heating
- T0xD9 Twin Tub Washing Machine
- T0xDA Impeller Washing Machine
- T0xDB Cylinder Washing Machine
- T0xDC Clothes Dryer
- T0xE1 Dishwasher
- T0xE2 Electric Water Heater
- T0xE3 Constant Temperature Gas Water Heater
- T0xEA Rice Cooker
- T0xED Water Softener
- T0xFA Electric Fan
- T0xFB Electric Heater
- T0xFD Humidifier
合作开发方法:添加本插件后,找到未能正确识别的设备,点击对应设备`传感器`分类下的`连通性`
Welcome to collaborate on adding support for more devices.
Collaboration method: After adding this plugin, find devices that are not correctly identified, click on `Connectivity` under the `Sensor` category of the corresponding device:
![img.png](./img/img.png)
展开下面的`属性`卡片把里面这些字段随issue提交。 着重关注Device type、Subtype这两个字段这是后续获得设备控制对应lua文件的基础。
Expand the `Attributes` card below and submit these fields with the issue. Pay special attention to the `Device type` and `Subtype` fields, as these are the basis for obtaining the corresponding lua files for device control.
再进入Homeassistant的安装目录, 在`.storage/midea_auto_cloud/lua/`目录下找到设备对应的T_0000_`Device type`_`Subtype`_***.lua文件等待适配就可以了。
Then go to the Home Assistant installation directory, find the device's corresponding T_0000_`Device type`_`Subtype`_***.lua file in the `.storage/midea_auto_cloud/lua/` directory, and wait for adaptation.
![img_1.png](./img/img_1.png)
## 实体映射
## Entity Mapping
映射文件位于`device_mapping`, 每个大的品类一个映射文件,目前支持映射的实体类型如下:
Mapping files are located under `device_mapping`, with one mapping file for each major category. Currently supported entity types for mapping are:
- sensor
- binary_sensor
- switch
@@ -63,8 +73,8 @@
- fan
- water_heater
示例配置`22012227`演示了如何将设备属性映射成以上各种HomeAssistant中的实体。
The example configuration `22012227` demonstrates how to map device attributes to various Home Assistant entities above.
## 致谢
## Acknowledgments
感谢[midea-meiju-codec](https://github.com/MattedBroadSky/midea-meiju-codec)项目提供的先验知识。
Thanks to the [midea-meiju-codec](https://github.com/MattedBroadSky/midea-meiju-codec) project for providing prior knowledge.

80
README_hans.md Normal file
View File

@@ -0,0 +1,80 @@
# Midea Auto Cloud
[![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.md) | 简体中文
通过网络获取 MSmartHome/美的美居 家庭中的设备并且通过美的云端API进行控制。
- 自动查找和发现设备
- 自动下载设备的协议文件
- 将设备状态更新为设备可见的属性
## 版本说明
- 所有设备默认可生成一个名为Status的二进制传感器其属性中列出了设备可访问的所有属性当然有些值不可设置
- Status实体前几项列出了该设备的分类信息供参考
## 目前支持的设备类型
- T0x13 电灯
- T0x15 养生壶
- T0x21 中央空调网关
- T0x26 浴霸
- T0x3D 电热水瓶
- T0x9B 蒸烤箱
- T0x9C 集成灶
- T0xA1 除湿机
- T0xAC 空调
- T0xB2 电蒸箱
- T0xB3 消毒碗柜
- T0xB6 抽油烟机
- T0xB7 燃气灶
- T0xB8 智能扫地机器人
- T0xCA 对开门冰箱
- T0xCC 中央空调(风管机)Wi-Fi线控器
- T0xCD 空气能热水器
- T0xCE 新风机
- T0xCF 中央空调暖家
- T0xD9 复式洗衣机
- T0xDA 波轮洗衣机
- T0xDB 滚筒洗衣机
- T0xDC 干衣机
- T0xE1 洗碗机
- T0xE2 电热水器
- T0xE3 恒温式燃气热水器
- T0xEA 电饭锅
- T0xED 软水机
- T0xFA 电风扇
- T0xFB 电暖器
- T0xFD 加湿器
欢迎合作开发添加更多设备支持。
合作开发方法:添加本插件后,找到未能正确识别的设备,点击对应设备`传感器`分类下的`连通性`
![img.png](./img/img.png)
展开下面的`属性`卡片把里面这些字段随issue提交。 着重关注Device type、Subtype这两个字段这是后续获得设备控制对应lua文件的基础。
再进入Homeassistant的安装目录`.storage/midea_auto_cloud/lua/`目录下找到设备对应的T_0000_`Device type`_`Subtype`_***.lua文件等待适配就可以了。
![img_1.png](./img/img_1.png)
## 实体映射
映射文件位于`device_mapping`下, 每个大的品类一个映射文件,目前支持映射的实体类型如下:
- sensor
- binary_sensor
- switch
- select
- climate
- fan
- water_heater
示例配置`22012227`演示了如何将设备属性映射成以上各种HomeAssistant中的实体。
## 致谢
感谢[midea-meiju-codec](https://github.com/MattedBroadSky/midea-meiju-codec)项目提供的先验知识。

View File

@@ -1,6 +1,7 @@
import os
import base64
from importlib import import_module
import re
from homeassistant.config_entries import ConfigEntry
from homeassistant.util.json import load_json
@@ -56,7 +57,9 @@ PLATFORMS: list[Platform] = [
Platform.WATER_HEATER,
Platform.FAN,
Platform.LIGHT,
Platform.HUMIDIFIER
Platform.HUMIDIFIER,
Platform.NUMBER,
Platform.BUTTON
]
@@ -96,14 +99,20 @@ async def load_device_config(hass: HomeAssistant, device_type, sn8):
# # 如果像映射体(包含 entities/centralized 等关键字段),直接使用
# if any(k in raw for k in ["entities", "centralized", "queries", "manufacturer"]):
# json_data = raw
if not json_data:
# if not json_data:
device_path = f".device_mapping.{'T0x%02X' % device_type}"
try:
mapping_module = import_module(device_path, __package__)
if sn8 in mapping_module.DEVICE_MAPPING.keys():
json_data = mapping_module.DEVICE_MAPPING[sn8]
elif "default" in mapping_module.DEVICE_MAPPING:
for key, config in mapping_module.DEVICE_MAPPING.items():
# support tuple & regular expression pattern to support multiple sn8 sharing one mapping
if (key == sn8) or (isinstance(key, tuple) and sn8 in key) or (isinstance(key, str) and re.match(key, sn8)):
json_data = config
break
if not json_data:
if "default" in mapping_module.DEVICE_MAPPING:
json_data = mapping_module.DEVICE_MAPPING["default"]
else:
MideaLogger.warning(f"No mapping found for sn8 {sn8} in type {'T0x%02X' % device_type}")
except ModuleNotFoundError:
MideaLogger.warning(f"Can't load mapping file for type {'T0x%02X' % device_type}")
@@ -131,20 +140,46 @@ 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):
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)
# if not os.path.exists(bit):
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)
return True
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

@@ -79,6 +79,10 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
self._key_max_temp = self._config.get("max_temp")
self._key_current_temperature = self._config.get("current_temperature")
self._key_target_temperature = self._config.get("target_temperature")
self._key_min_humidity = self._config.get("min_humidity")
self._key_max_humidity = self._config.get("max_humidity")
self._key_current_humidity = self._config.get("current_humidity")
self._key_target_humidity = self._config.get("target_humidity")
self._attr_temperature_unit = self._config.get("temperature_unit")
self._attr_precision = self._config.get("precision")
@@ -89,6 +93,8 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
features |= ClimateEntityFeature.TURN_OFF
if self._key_target_temperature is not None:
features |= ClimateEntityFeature.TARGET_TEMPERATURE
if self._key_target_humidity is not None:
features |= ClimateEntityFeature.TARGET_HUMIDITY
if self._key_preset_modes is not None:
features |= ClimateEntityFeature.PRESET_MODE
# if self._key_aux_heat is not None:
@@ -137,6 +143,28 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
return None
return None
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
humidity = self._get_nested_value(self._key_current_humidity)
if humidity is not None:
try:
return float(humidity)
except (ValueError, TypeError):
return None
return None
@property
def target_humidity(self) -> float | None:
"""Return the humidity we try to reach."""
humidity = self._get_nested_value(self._key_target_humidity)
if humidity is not None:
try:
return float(humidity)
except (ValueError, TypeError):
return None
return None
@property
def min_temp(self):
if isinstance(self._key_min_temp, str):
@@ -151,6 +179,20 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
else:
return float(self._key_max_temp)
@property
def min_humidity(self):
if isinstance(self._key_min_humidity, str):
return float(self.device_attributes.get(self._key_min_humidity, 45))
else:
return float(self._key_min_humidity)
@property
def max_humidity(self):
if isinstance(self._key_max_humidity, str):
return float(self.device_attributes.get(self._key_max_humidity, 65))
else:
return float(self._key_max_humidity)
@property
def target_temperature_low(self):
return self.min_temp
@@ -159,6 +201,14 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
def target_temperature_high(self):
return self.max_temp
@property
def target_humidity_low(self):
return self.min_humidity
@property
def target_humidity_high(self):
return self.max_humidity
@property
def preset_modes(self):
return list(self._key_preset_modes.keys())
@@ -253,6 +303,13 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
new_status[self._key_target_temperature] = temperature
await self.async_set_attributes(new_status)
async def async_set_humidity(self, humidity: int):
if self._key_target_humidity is None:
return
new_status = {}
new_status[self._key_target_humidity] = int(humidity)
await self.async_set_attributes(new_status)
async def async_set_fan_mode(self, fan_mode: str):
if self._is_central_ac:
fan_speed = self._key_fan_modes.get(fan_mode)

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:
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"
@@ -354,11 +361,8 @@ class MeijuCloud(MideaCloud):
command_data = {
"nodeid": nodeid,
"acattri_ctrl": {
"aclist": [{
"nodeid": nodeid,
"modelid": modelid,
"type": idtype
}],
"modelid": modelid, "type": idtype, "aclist_data": nodeid[-2:],
"event": control
}
}
@@ -405,6 +409,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

@@ -295,19 +295,20 @@ class MiedaDevice(threading.Thread):
break
if calculate:
calculate_str1 = \
(f"{lvalue.replace('[', 'self._attributes[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[", "[\"").replace("]", "\"]")
(f"{lvalue.replace('[', 'self._attributes[').replace("]", "\"]")} = "
f"{rvalue.replace('[', 'float(self._attributes[').replace(']', "\"])")}") \
.replace("[", "[\"")
calculate_str2 = \
(f"{lvalue.replace('[', 'new_status[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[", "[\"").replace("]", "\"]")
(f"{lvalue.replace('[', 'new_status[').replace("]", "\"]")} = "
f"{rvalue.replace('[', 'float(self._attributes[').replace(']', "\"])")}") \
.replace("[", "[\"")
try:
exec(calculate_str1)
exec(calculate_str2)
except Exception:
except Exception as e:
traceback.print_exc()
MideaLogger.warning(
f"Calculation Error: {lvalue} = {rvalue}", self._device_id
f"Calculation Error: {lvalue} = {rvalue}, calculate_str1: {calculate_str1}, calculate_str2: {calculate_str2}", self._device_id
)
self._update_all(new_status)
return ParseMessageResult.SUCCESS

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()
@@ -70,6 +87,17 @@ class MideaCodec(LuaRuntime):
query_dict["control"]["bucket"] = prefix
else:
query_dict["control"]["bucket"] = "db"
# 针对T0x9C集成灶特殊处理
elif self._device_type == "T0x9C":
control_keys = list(append.keys())
if len(control_keys) > 0:
# 从第一个键名中提取前缀,例如从 'db_power' 中提取 'db'
first_key = control_keys[0]
prefix = first_key.split("_")[0]
else:
prefix = "total"
query_dict["control"]["type"] = prefix
json_str = json.dumps(query_dict)
MideaLogger.debug(f"LuaRuntime json_str {json_str}")
try:

View File

@@ -1,9 +1,11 @@
"""Data coordinator for Midea Auto Cloud integration."""
import logging
import traceback
from datetime import datetime, timedelta
from typing import NamedTuple
from attr import attributes
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
@@ -123,9 +125,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 +135,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,20 +154,67 @@ 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}")
async def async_set_attribute(self, attribute: str, value) -> None:
"""Set a device attribute."""
# 云端控制:构造 control 与 status携带当前状态作为上下文
await self.device.set_attribute(attribute, value)
self.device.attributes[attribute] = value
self.mute_state_update_for_a_while()
self.async_update_listeners()
attributes = {}
attributes[attribute] = value
await self.async_set_attributes(attributes)
async def async_set_attributes(self, attributes: dict) -> None:
"""Set multiple device attributes."""
# 云端控制:构造 control 与 status携带当前状态作为上下文
for c in self.device._calculate_set:
lvalue = c.get("lvalue")
rvalue = c.get("rvalue")
if lvalue and rvalue:
calculate = False
for s, v in attributes.items():
if rvalue.find(f"[{s}]") >= 0:
calculate = True
break
if calculate:
calculate_str1 = \
(f"{lvalue.replace('[', 'attributes[').replace("]", "\"]")} = "
f"{rvalue.replace('[', 'float(attributes[').replace(']', "\"])")}") \
.replace("[", "[\"")
try:
exec(calculate_str1)
except Exception as e:
traceback.print_exc()
MideaLogger.warning(
f"Calculation Error: {lvalue} = {rvalue}, calculate_str1: {calculate_str1}",
self._device_id
)
await self.device.set_attributes(attributes)
self.device.attributes.update(attributes)
self.mute_state_update_for_a_while()
@@ -223,6 +271,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,14 +45,7 @@ DEVICE_MAPPING = {
Platform.FAN: {
"fan": {
"power": "fan_power",
"fan_speed": [
{"fan_speed": "1"},
{"fan_speed": "2"},
{"fan_speed": "3"},
{"fan_speed": "4"},
{"fan_speed": "5"},
{"fan_speed": "6"},
],
"speeds": list({"fan_speed": value + 1} for value in range(0, 6)),
"preset_modes": {
"breathing_wind": {"fan_scene": "breathing_wind"},
"const_temperature": {"fan_scene": "const_temperature"},

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

@@ -0,0 +1,20 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfVolume, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \
UnitOfEnergy, UnitOfPower, PRECISION_WHOLE, UnitOfPressure
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.SENSOR: {
"work_status": {
"device_class": SensorDeviceClass.ENUM,
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfVolume, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \
UnitOfEnergy, UnitOfPower, PRECISION_WHOLE, UnitOfPressure
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": ["b6_light"],
"entities": {
Platform.NUMBER: {
"b6_lightness": {
"min": 0,
"max": 100,
"step": 1
}
},
Platform.SWITCH: {
"b6_light": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.SELECT: {
"b6_gear": {
"options": {
"": {"b6_gear": "0"},
"": {"b6_gear": "1"},
"": {"b6_gear": "2"},
"": {"b6_gear": "3"},
"爆炒": {"b6_gear": "4"}
}
},
"b6_power_on_light": {
"options": {
"": {"b6_power_on_light": "off", "b6_setting": "power_on_light"},
"": {"b6_power_on_light": "on", "b6_setting": "power_on_light"}
}
},
"b6_lock": {
"options": {
"": {"b6_lock": "off", "b6_setting": "lock"},
"": {"b6_lock": "on", "b6_setting": "lock"}
}
},
"b6_smoke_stove_linkage_gear": {
"options": {
"1档": {"b6_smoke_stove_linkage_gear": 1, "b6_setting": "smoke_stove_linkage"},
"2档": {"b6_smoke_stove_linkage_gear": 2, "b6_setting": "smoke_stove_linkage"},
"3档": {"b6_smoke_stove_linkage_gear": 3, "b6_setting": "smoke_stove_linkage"},
}
},
"b6_delay_gear_linkage_gear": {
"options": {
"1档": {"b6_delay_gear_linkage_gear": 1, "b6_setting": "delay_gear_linkage"},
"2档": {"b6_delay_gear_linkage_gear": 2, "b6_setting": "delay_gear_linkage"},
"3档": {"b6_delay_gear_linkage_gear": 3, "b6_setting": "delay_gear_linkage"},
}
},
"b6_delay_time_value": {
"options": {
"": {"b6_delay_time": "off", "b6_setting": "delay_time"},
"1分钟": {"b6_delay_time": "on", "b6_delay_time_value": 1, "b6_setting": "delay_time"},
"2分钟": {"b6_delay_time": "on", "b6_delay_time_value": 2, "b6_setting": "delay_time"},
"3分钟": {"b6_delay_time": "on", "b6_delay_time_value": 3, "b6_setting": "delay_time"},
"4分钟": {"b6_delay_time": "on", "b6_delay_time_value": 4, "b6_setting": "delay_time"},
"5分钟": {"b6_delay_time": "on", "b6_delay_time_value": 5, "b6_setting": "delay_time"},
"6分钟": {"b6_delay_time": "on", "b6_delay_time_value": 6, "b6_setting": "delay_time"},
"7分钟": {"b6_delay_time": "on", "b6_delay_time_value": 7, "b6_setting": "delay_time"},
"8分钟": {"b6_delay_time": "on", "b6_delay_time_value": 8, "b6_setting": "delay_time"},
"9分钟": {"b6_delay_time": "on", "b6_delay_time_value": 9, "b6_setting": "delay_time"},
"10分钟": {"b6_delay_time": "on", "b6_delay_time_value": 10, "b6_setting": "delay_time"},
}
}
},
Platform.SENSOR: {
"b6_wind_pressure": {
"device_class": SensorDeviceClass.PRESSURE,
"unit_of_measurement": UnitOfPressure.PA,
"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
@@ -12,13 +12,7 @@ DEVICE_MAPPING = {
Platform.FAN: {
"fan": {
"power": "new_wind_machine",
"speeds": [
{"fresh_air_fan_speed": 20},
{"fresh_air_fan_speed": 40},
{"fresh_air_fan_speed": 60},
{"fresh_air_fan_speed": 80},
{"fresh_air_fan_speed": 100},
],
"speeds": list({"fresh_air_fan_speed": value + 1} for value in range(0, 100)),
"preset_modes": {
"heat_exchange": {
"fresh_air_mode": 1,
@@ -132,6 +126,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"}],
@@ -140,13 +160,7 @@ DEVICE_MAPPING = {
Platform.FAN: {
"fan": {
"power": "fresh_air",
"speeds": [
{"fresh_air": 3, "fresh_air_fan_speed": 20},
{"fresh_air": 3, "fresh_air_fan_speed": 40},
{"fresh_air": 3, "fresh_air_fan_speed": 60},
{"fresh_air": 3, "fresh_air_fan_speed": 80},
{"fresh_air": 3, "fresh_air_fan_speed": 100},
],
"speeds": list({"fresh_air": 3, "fresh_air_fan_speed": value + 1} for value in range(0, 100)),
"preset_modes": {
"heat_exchange": {
"fresh_air_mode": 1,
@@ -267,5 +281,86 @@ DEVICE_MAPPING = {
},
}
}
},
# Colmo Turing Central AC indoor units, different cooling capacity models share the same config.
("22396961", "22396963", "22396965", "22396969", "22396973"): {
"rationale": ["off", "on"],
"queries": [{}, {"query_type":"run_status"}],
"centralized": [],
"entities": {
Platform.CLIMATE: {
"thermostat": {
"translation_key": "colmo_turing_central_ac_climate",
"power": "power",
"hvac_modes": {
"off": {"power": "off"},
"heat": {"power": "on", "mode": "heat"},
"cool": {"power": "on", "mode": "cool"},
"fan_only": {"power": "on", "mode": "fan"},
"dry": {"power": "on", "mode": "dryauto"},
"auto": {"power": "on", "mode": "dryconstant"},
# Note:
# For Colmo Turing AC, dry and auto mode is not displayed in the app/controller explicitly.
# Instead it defined 2 custom modes: dryauto (自动抽湿) and dryconstant (温湿灵控/恒温恒湿).
# So I mapped the custom modes to the similar pre-defineds:
# - auto -> dryconstant (温湿灵控/恒温恒湿): able to set target T and H, and auto adjust them to maintain a comfortable environment.
# - dry -> dryauto (自动抽湿): dehumidification mode, under which temperature is not adjustable.
# Translations are also modified (for only colmo_turing_central_ac_climate) accordingly.
},
"preset_modes": {
"none": {
"energy_save": "off",
},
"sleep": {"energy_save": "on"}
},
"fan_modes": {
"silent": {"wind_speed": 20},
"low": {"wind_speed": 40},
"medium": {"wind_speed": 60},
"high": {"wind_speed": 80},
"full": {"wind_speed": 100},
"auto": {"wind_speed": 102}
},
"target_temperature": ["temperature", "small_temperature"],
"current_temperature": "indoor_temperature",
"target_humidity": "dehumidity",
"current_humidity": "indoor_humidity",
"pre_mode": "mode",
"aux_heat": "ptc",
"min_temp": 16,
"max_temp": 30,
"min_humidity": 45,
"max_humidity": 65,
"temperature_unit": UnitOfTemperature.CELSIUS,
"precision": PRECISION_HALVES,
}
},
Platform.SENSOR: {
"mode": {
"device_class": SensorDeviceClass.ENUM,
},
"indoor_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"indoor_humidity": {
"device_class": SensorDeviceClass.HUMIDITY,
"unit_of_measurement": "%",
"state_class": SensorStateClass.MEASUREMENT
},
"wind_speed_real": {
"device_class": SensorDeviceClass.WIND_SPEED,
"unit_of_measurement": "%",
"state_class": SensorStateClass.MEASUREMENT
}
},
Platform.SWITCH: {
"power": {
"name": "电源",
"device_class": SwitchDeviceClass.SWITCH,
},
},
}
}
}

View File

@@ -0,0 +1,69 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE, PRECISION_HALVES, 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": {
"manufacturer": "美的",
"rationale": ["off", "on"],
"queries": [{}],
"calculate": {
"get": [
{
"lvalue": "[temperature]",
"rvalue": "([set_temperature] - 106) / 74 * 37 + 38"
},
{
"lvalue": "[cur_temperature]",
"rvalue": "([water_box_temperature] - 106) / 74 * 37 + 38"
}
],
"set": [
{
"lvalue": "[set_temperature]",
"rvalue": "([temperature] - 38) / 37 * 74 + 106"
},
]
},
"centralized": [],
"entities": {
Platform.CLIMATE: {
"water_heater": {
"power": "power",
"hvac_modes": {
"off": {"power": "off"},
"heat": {"power": "on"},
},
"preset_modes": {
"标准": {"mode": "standard"},
"节能": {"mode": "energy"},
"速热": {"mode": "compatibilizing"},
},
"target_temperature": "temperature",
"current_temperature": "cur_temperature",
"min_temp": 38,
"max_temp": 75,
"temperature_unit": UnitOfTemperature.CELSIUS,
"precision": PRECISION_WHOLE,
}
},
Platform.SWITCH: {
"power": {
"device_class": SwitchDeviceClass.SWITCH,
},
"mute": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.SENSOR: {
"cur_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"unit_of_measurement": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
},
}
}
}

View File

@@ -0,0 +1,150 @@
from homeassistant.const import Platform, UnitOfElectricPotential, UnitOfTemperature, UnitOfTime
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": [{}],
"calculate": {
"get": [
{
"lvalue": "[remain_time]",
"rvalue": "[remain_time]"
}
],
"set": {
}
},
"entities": {
Platform.NUMBER: {
"temperature": {
"min": 0,
"max": 100,
"step": 1
},
"detergent": {
"min": 0,
"max": 5,
"step": 1
},
"softener": {
"min": 0,
"max": 5,
"step": 1
},
"dehydration_speed": {
"min": 0,
"max": 1600,
"step": 100
},
"soak_time": {
"min": 0,
"max": 40,
"step": 10
},
"wash_time": {
"min": 0,
"max": 20,
"step": 1
},
"rinse_count": {
"min": 0,
"max": 3,
"step": 1
},
"dehydration_time": {
"min": 0,
"max": 9,
"step": 1
},
"wash_level": {
"min": 0,
"max": 8,
"step": 1
},
"rinse_level": {
"min": 0,
"max": 8,
"step": 1
},
"wash_strength": {
"min": 1,
"max": 4,
"step": 1
},
},
Platform.BINARY_SENSOR: {
"softener_lack": {
"device_class": BinarySensorDeviceClass.PROBLEM,
},
"detergent_lack": {
"device_class": BinarySensorDeviceClass.PROBLEM,
},
"door_opened": {
"device_class": BinarySensorDeviceClass.PROBLEM,
},
"bucket_water_overheating": {
"device_class": BinarySensorDeviceClass.PROBLEM,
},
},
Platform.SWITCH: {
"power": {
"device_class": SwitchDeviceClass.SWITCH,
},
"control_status": {
"rationale": ["pause", "start"],
},
"lock": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.SELECT: {
"mode": {
"options": {
"normal": {"mode": "normal"},
"dry": {"mode": "dry"},
"continus": {"mode": "continus"},
}
},
"program": {
"options": {
"标准": {"program": "standard"},
"速洗": {"program": "fast"},
"家纺": {"program": "blanket"},
"羊毛": {"program": "wool"},
"浸洗": {"program": "embathe"},
"记忆": {"program": "memory"},
"童装": {"program": "child"},
"强洗": {"program": "strong_wash"},
"桶自洁": {"program": "bucket_self_clean"},
}
},
},
Platform.SENSOR: {
"running_status": {
"device_class": SensorDeviceClass.ENUM
},
"appointment_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"remain_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"progress": {
"device_class": SensorDeviceClass.BATTERY,
"unit_of_measurement": "%",
"state_class": SensorStateClass.MEASUREMENT
},
"error_code": {
"device_class": SensorDeviceClass.ENUM
},
}
}
}
}

View File

@@ -18,6 +18,48 @@ DEVICE_MAPPING = {
}
},
"entities": {
Platform.NUMBER: {
"temperature": {
"min": 0,
"max": 100,
"step": 1
},
"detergent": {
"min": 0,
"max": 5,
"step": 1
},
"softener": {
"min": 0,
"max": 5,
"step": 1
},
"dehydration_time": {
"min": 0,
"max": 9,
"step": 1
},
"dehydration_speed": {
"min": 0,
"max": 1600,
"step": 100
},
"dirty_degree": {
"min": 0,
"max": 4,
"step": 1
},
"soak_count": {
"min": 0,
"max": 5,
"step": 1
},
"wash_time": {
"min": 0,
"max": 20,
"step": 1
},
},
Platform.BINARY_SENSOR: {
"softener_lack": {
"device_class": BinarySensorDeviceClass.PROBLEM,
@@ -66,19 +108,6 @@ DEVICE_MAPPING = {
},
},
Platform.SELECT: {
"dehydration_speed": {
"options": {
"0": {"dehydration_speed": "0"},
"400": {"dehydration_speed": "400"},
"600": {"dehydration_speed": "600"},
"800": {"dehydration_speed": "800"},
"1000": {"dehydration_speed": "1000"},
"1200": {"dehydration_speed": "1200"},
"1400": {"dehydration_speed": "1400"},
"1600": {"dehydration_speed": "1600"},
"1300": {"dehydration_speed": "1300"}
}
},
"mode": {
"options": {
"normal": {"mode": "normal"},
@@ -190,68 +219,11 @@ DEVICE_MAPPING = {
"hanfu_wash": {"program": "hanfu_wash"}
}
},
"temperature": {
"options": {
"0": {"temperature": "0"},
"20": {"temperature": "20"},
"30": {"temperature": "30"},
"40": {"temperature": "40"},
"50": {"temperature": "50"},
"60": {"temperature": "60"},
"70": {"temperature": "70"},
"90": {"temperature": "90"},
"95": {"temperature": "95"}
}
},
"detergent": {
"options": {
"0": {"detergent": "0"},
"1": {"detergent": "1"},
"2": {"detergent": "2"},
"3": {"detergent": "3"},
"4": {"detergent": "4"},
"5": {"detergent": "5"}
}
},
"softener": {
"options": {
"0": {"softener": "0"},
"1": {"softener": "1"},
"2": {"softener": "2"},
"3": {"softener": "3"},
"4": {"softener": "4"},
"5": {"softener": "5"}
}
},
"dirty_degree": {
"options": {
"0": {"dirty_degree": "0"},
"1": {"dirty_degree": "1"},
"2": {"dirty_degree": "2"},
"3": {"dirty_degree": "3"},
"4": {"dirty_degree": "4"}
}
},
"soak_count": {
"options": {
"0": {"soak_count": "0"},
"1": {"soak_count": "1"},
"2": {"soak_count": "2"},
"3": {"soak_count": "3"},
"4": {"soak_count": "4"},
"5": {"soak_count": "5"}
}
}
},
Platform.SENSOR: {
"running_status": {
"device_class": SensorDeviceClass.ENUM
},
"wash_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"appointment_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
@@ -273,11 +245,6 @@ DEVICE_MAPPING = {
"unit_of_measurement": "%",
"state_class": SensorStateClass.MEASUREMENT
},
"dehydration_time_value": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"customize_machine_cycle": {
"device_class": SensorDeviceClass.ENUM
},
@@ -315,11 +282,6 @@ DEVICE_MAPPING = {
"active_oxygen": {
"device_class": SensorDeviceClass.ENUM
},
"dehydration_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"cloud_cycle_jiepai_time2": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,

View File

@@ -10,12 +10,23 @@ DEVICE_MAPPING = {
"queries": [{}],
"centralized": [],
"entities": {
Platform.WATER_HEATER: {
Platform.NUMBER: {
"water_quality": {
"min": 0,
"max": 3,
"step": 1
},
"cur_temperature": {
"device_class": SensorDeviceClass.TEMPERATURE,
"state_class": SensorStateClass.MEASUREMENT,
}
},
Platform.CLIMATE: {
"water_heater": {
"power": "power",
"operation_list": {
"hvac_modes": {
"off": {"power": "off"},
"on": {"power": "on"},
"heat": {"power": "on"},
},
"target_temperature": "temperature",
"current_temperature": "cur_temperature",
@@ -26,6 +37,9 @@ DEVICE_MAPPING = {
}
},
Platform.SWITCH: {
"power": {
"device_class": SwitchDeviceClass.SWITCH,
},
"ti_protect": {
"device_class": SwitchDeviceClass.SWITCH,
},
@@ -49,14 +63,6 @@ DEVICE_MAPPING = {
},
},
Platform.SELECT: {
"water_quality": {
"options": {
"0": {"water_quality": 0},
"1": {"water_quality": 1},
"2": {"water_quality": 2},
"3": {"water_quality": 3}
}
},
"func_select": {
"options": {
"low": {"func_select": "low"},

View File

@@ -10,6 +10,9 @@ DEVICE_MAPPING = {
"centralized": [],
"entities": {
Platform.SWITCH: {
"power": {
"device_class": SwitchDeviceClass.SWITCH,
},
"bubble": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": [0, 1]

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

@@ -1,4 +1,4 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE
from homeassistant.const import Platform, UnitOfTemperature, UnitOfTime, PERCENTAGE, DEGREE
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
@@ -8,34 +8,28 @@ 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": list({"gear": value + 1} for value in range(0, 9)),
"oscillate": "swing",
"preset_modes": {
"normal": {"mode": "normal"},
"sleep": {"mode": "sleep"},
"baby": {"mode": "baby"}
}
}
},
Platform.SELECT: {
@@ -65,16 +59,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 +67,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 +83,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,
@@ -142,30 +92,6 @@ DEVICE_MAPPING = {
"device_class": SensorDeviceClass.ENUM,
"state_class": SensorStateClass.MEASUREMENT
},
"timer_off_hour": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"timer_off_minute": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.MINUTES,
"state_class": SensorStateClass.MEASUREMENT
},
"timer_on_hour": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"timer_on_minute": {
"device_class": SensorDeviceClass.DURATION,
"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,26 +102,107 @@ 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
}
}
}
},
"xxxxxx": {
"rationale": ["off", "on"],
"queries": [{}],
"centralized": [
"power", "gear"
],
"entities": {
Platform.SWITCH: {
"display_on_off": {
"device_class": SwitchDeviceClass.SWITCH,
"rationale": ["on", "off"]
},
"anion": {
"device_class": SwitchDeviceClass.SWITCH,
},
"temp_wind_switch": {
"device_class": SwitchDeviceClass.SWITCH,
},
},
Platform.FAN: {
"fan": {
"power": "power",
"speeds": list({"gear": value + 1} for value in range(0, 100)),
"preset_modes": {
"self_selection": {"mode": "self_selection"},
"sleeping_wind": {"mode": "sleeping_wind"},
"purified_wind": {"mode": "purified_wind"}
}
}
},
Platform.SELECT: {
"voice": {
"options": {
"open_buzzer": {"voice": "open_buzzer"},
"close_buzzer": {"voice": "close_buzzer"},
"mute": {"voice": "mute"}
}
},
"lr_shake_switch": {
"options": {
"off": {"lr_shake_switch": "off"},
"default": {"lr_shake_switch": "default"},
"normal": {"lr_shake_switch": "normal"},
}
},
"ud_shake_switch": {
"options": {
"off": {"ud_shake_switch": "off"},
"default": {"ud_shake_switch": "default"},
"normal": {"ud_shake_switch": "normal"},
}
},
},
Platform.SENSOR: {
"real_gear": {
"device_class": SensorDeviceClass.ENUM,
"state_class": SensorStateClass.MEASUREMENT
},
"dust_life_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"filter_life_time": {
"device_class": SensorDeviceClass.DURATION,
"unit_of_measurement": UnitOfTime.HOURS,
"state_class": SensorStateClass.MEASUREMENT
},
"current_angle": {
"device_class": SensorDeviceClass.WIND_DIRECTION,
"unit_of_measurement": DEGREE,
"state_class": SensorStateClass.MEASUREMENT
},
"target_angle": {
"device_class": SensorDeviceClass.WIND_DIRECTION,
"unit_of_measurement": DEGREE,
"state_class": SensorStateClass.MEASUREMENT
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
from homeassistant.const import Platform, UnitOfTemperature, UnitOfVolume, UnitOfTime, PERCENTAGE, PRECISION_HALVES, \
UnitOfEnergy, UnitOfPower
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: {
"lock": {
"device_class": SwitchDeviceClass.SWITCH,
},
"screen_close": {
"device_class": SwitchDeviceClass.SWITCH,
}
},
Platform.CLIMATE: {
"electric_heater": {
"power": "power",
"hvac_modes": {
"off": {"power": "off"},
"heat": {"power": "on"}
},
"target_temperature": "temperature",
"current_temperature": "cur_temperature",
"min_temp": 5,
"max_temp": 35,
"temperature_unit": UnitOfTemperature.CELSIUS,
"precision": PRECISION_HALVES,
}
},
Platform.SELECT: {
"gear": {
"options": {
"low": {"gear": 1},
"high": {"gear": 3}
}
}
},
Platform.SENSOR: {
"power_statistics": {
"device_class": SensorDeviceClass.POWER,
"unit_of_measurement": UnitOfPower.WATT,
"state_class": SensorStateClass.MEASUREMENT
}
}
}
}
}

View File

@@ -54,7 +54,18 @@ class MideaFanEntity(MideaEntity, FanEntity):
)
self._key_power = self._config.get("power")
self._key_preset_modes = self._config.get("preset_modes")
self._key_speeds = self._config.get("speeds")
speeds_config = self._config.get("speeds")
# 处理范围形式的 speeds 配置: {"key": "gear", "value": [1, 9]}
if isinstance(speeds_config, dict) and "key" in speeds_config and "value" in speeds_config:
key_name = speeds_config["key"]
value_range = speeds_config["value"]
if isinstance(value_range, list) and len(value_range) == 2:
start, end = value_range[0], value_range[1]
self._key_speeds = [{key_name: str(i)} for i in range(start, end + 1)]
else:
self._key_speeds = speeds_config
else:
self._key_speeds = speeds_config
self._key_oscillate = self._config.get("oscillate")
self._key_directions = self._config.get("directions")
self._attr_speed_count = len(self._key_speeds) if self._key_speeds else 0

View File

@@ -0,0 +1,26 @@
{
"entity": {
"climate": {
"thermostat": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "mdi:weather-night",
"full": "mdi:fan"
}
}
}
},
"colmo_turing_central_ac_climate": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "mdi:weather-night",
"full": "mdi:fan"
}
}
}
}
}
}
}

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.20"
}

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)
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)
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"
},
@@ -336,10 +312,32 @@
"name": "Storage Zone"
},
"thermostat": {
"name": "Thermostat"
"name": "Thermostat",
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Silent",
"full": "Full"
}
}
}
},
"colmo_turing_central_ac_climate": {
"name": "Thermostat",
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Silent",
"full": "Full"
}
}
}
},
"water_heater": {
"name": "Water Heater"
},
"electric_heater": {
"name": "Electric Heater"
}
},
"humidifier": {
@@ -351,6 +349,33 @@
}
},
"select": {
"b6_power": {
"name": "Smoke Machine Power"
},
"b6_power_on_light": {
"name": "Smoke Machine Power On Light"
},
"b6_lock": {
"name": "Smoke Machine Lock"
},
"b6_gear": {
"name": "Smoke Machine Gear"
},
"b6_smoke_stove_linkage_gear": {
"name": "Smoke Stove Linkage Gear"
},
"b6_delay_gear_linkage_gear": {
"name": "Smoke Machine Delay Gear Linkage Gear"
},
"b6_delay_time_value": {
"name": "Smoke Machine Delay Time (Minute)"
},
"warm_target_temp": {
"name": "Warm Target Temperature"
},
"boil_target_temp": {
"name": "Boil Target Temperature"
},
"add_rinse": {
"name": "Add Rinse"
},
@@ -363,12 +388,6 @@
"control_type": {
"name": "Control Type"
},
"dehydration_speed": {
"name": "Dehydration Speed"
},
"db_dehydration_speed": {
"name": "DB Dehydration Speed"
},
"db_detergent": {
"name": "DB Detergent"
},
@@ -399,9 +418,6 @@
"detergent_density": {
"name": "Detergent Density"
},
"dirty_degree": {
"name": "Dirty Degree"
},
"disinfectant": {
"name": "Disinfectant"
},
@@ -486,9 +502,6 @@
"season": {
"name": "Season"
},
"soak_count": {
"name": "Soak Count"
},
"softener_density": {
"name": "Softener Density"
},
@@ -540,9 +553,6 @@
"type_select": {
"name": "Type Select"
},
"water_quality": {
"name": "Water Quality"
},
"work_status": {
"name": "Work Status"
},
@@ -567,15 +577,6 @@
"gear": {
"name": "Gear"
},
"detergent": {
"name": "Detergent"
},
"softener": {
"name": "Softener"
},
"temperature": {
"name": "Temperature"
},
"ptc": {
"name": "PTC"
},
@@ -1505,12 +1506,74 @@
"name": "Light"
}
},
"number": {
"water_quality": {
"name": "Water Quality"
},
"b6_lightness": {
"name": "Smoke Machine Lightness"
},
"dehydration_speed": {
"name": "Dehydration Speed"
},
"db_dehydration_speed": {
"name": "DB Dehydration Speed"
},
"detergent": {
"name": "Detergent"
},
"softener": {
"name": "Softener"
},
"temperature": {
"name": "Temperature"
},
"soak_count": {
"name": "Soak Count"
},
"dirty_degree": {
"name": "Dirty Degree"
},
"soak_time": {
"name": "Soak Time"
},
"wash_time": {
"name": "Wash Time"
},
"rinse_count": {
"name": "Rinse Count"
},
"dehydration_time": {
"name": "Dehydration Time"
},
"wash_level": {
"name": "Wash Level"
},
"rinse_level": {
"name": "Rinse Level"
},
"wash_strength": {
"name": "Water Strength"
}
},
"fan": {
"fan": {
"name": "Fan"
}
},
"switch": {
"mute": {
"name": "Mute"
},
"b6_light": {
"name": "Smoke Machine Light"
},
"temp_wind_switch": {
"name": "Wind Change with Temperature"
},
"screen_close": {
"name": "Screen Close"
},
"anion": {
"name": "Anion"
},
@@ -2362,6 +2425,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": "加热状态"
},
@@ -336,10 +312,36 @@
"name": "冷藏区"
},
"thermostat": {
"name": "温控器"
"name": "温控器",
"state_attributes": {
"fan_mode": {
"state": {
"silent": "静音",
"full": "强劲"
}
}
}
},
"colmo_turing_central_ac_climate": {
"name": "温控器",
"state_attributes": {
"fan_mode": {
"state": {
"silent": "静音",
"full": "强劲"
}
}
},
"state": {
"auto": "温湿灵控",
"dry": "自动抽湿"
}
},
"water_heater": {
"name": "热水器"
},
"electric_heater": {
"name": "取暖器"
}
},
"humidifier": {
@@ -351,6 +353,33 @@
}
},
"select": {
"b6_power": {
"name": "烟机开关"
},
"b6_power_on_light": {
"name": "烟机开机开灯"
},
"b6_lock": {
"name": "烟机锁屏"
},
"b6_gear": {
"name": "烟机档位"
},
"b6_smoke_stove_linkage_gear": {
"name": "烟灶联动档位"
},
"b6_delay_gear_linkage_gear": {
"name": "烟机延时关机风速档位"
},
"b6_delay_time_value": {
"name": "烟机延时关机时间(分钟)"
},
"warm_target_temp": {
"name": "保温目标温度"
},
"boil_target_temp": {
"name": "煮沸目标温度"
},
"add_rinse": {
"name": "加漂洗"
},
@@ -363,12 +392,6 @@
"control_type": {
"name": "控制类型"
},
"dehydration_speed": {
"name": "脱水转速"
},
"db_dehydration_speed": {
"name": "脱水转速"
},
"db_detergent": {
"name": "洗涤剂"
},
@@ -399,9 +422,6 @@
"detergent_density": {
"name": "洗涤剂浓度"
},
"dirty_degree": {
"name": "脏污程度"
},
"disinfectant": {
"name": "消毒剂"
},
@@ -486,9 +506,6 @@
"season": {
"name": "季节"
},
"soak_count": {
"name": "浸泡次数"
},
"softener_density": {
"name": "柔顺剂浓度"
},
@@ -543,9 +560,6 @@
"type_select": {
"name": "类型选择"
},
"water_quality": {
"name": "水质"
},
"work_status": {
"name": "工作状态"
},
@@ -567,15 +581,6 @@
"gear": {
"name": "档位"
},
"detergent": {
"name": "洗涤剂"
},
"softener": {
"name": "柔顺剂"
},
"temperature": {
"name": "温度"
},
"ptc": {
"name": "电辅热"
},
@@ -1505,12 +1510,74 @@
"name": "电灯"
}
},
"number": {
"water_quality": {
"name": "水质"
},
"b6_lightness": {
"name": "烟机照明"
},
"dehydration_speed": {
"name": "脱水转速"
},
"db_dehydration_speed": {
"name": "脱水转速"
},
"detergent": {
"name": "洗涤剂"
},
"softener": {
"name": "柔顺剂"
},
"temperature": {
"name": "温度"
},
"soak_count": {
"name": "浸泡次数"
},
"dirty_degree": {
"name": "脏污程度"
},
"soak_time": {
"name": "浸泡时间"
},
"wash_time": {
"name": "洗涤时间"
},
"rinse_count": {
"name": "漂洗次数"
},
"dehydration_time": {
"name": "脱水时间"
},
"wash_level": {
"name": "洗涤水位"
},
"rinse_level": {
"name": "漂洗水位"
},
"wash_strength": {
"name": "水流强度"
}
},
"fan": {
"fan": {
"name": "风扇"
}
},
"switch": {
"mute": {
"name": "静音"
},
"b6_light": {
"name": "烟机灯"
},
"temp_wind_switch": {
"name": "风随温变"
},
"screen_close": {
"name": "屏幕关闭"
},
"anion": {
"name": "负离子"
},
@@ -2362,6 +2429,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": "工作开关"
}
}
}