2023-09-02 16:30:03 +08:00
|
|
|
|
import os
|
|
|
|
|
import base64
|
2023-09-10 12:08:27 +08:00
|
|
|
|
from importlib import import_module
|
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2023-09-09 00:14:41 +08:00
|
|
|
|
from homeassistant.util.json import load_json
|
2025-09-17 23:15:15 +08:00
|
|
|
|
|
2023-09-09 00:14:41 +08:00
|
|
|
|
try:
|
|
|
|
|
from homeassistant.helpers.json import save_json
|
|
|
|
|
except ImportError:
|
|
|
|
|
from homeassistant.util.json import save_json
|
2023-09-10 12:08:27 +08:00
|
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2025-09-12 00:15:14 +08:00
|
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
2023-09-17 19:40:54 +08:00
|
|
|
|
from homeassistant.core import (
|
2025-09-17 23:15:15 +08:00
|
|
|
|
HomeAssistant,
|
2023-09-17 19:40:54 +08:00
|
|
|
|
)
|
2023-09-02 16:30:03 +08:00
|
|
|
|
from homeassistant.const import (
|
|
|
|
|
Platform,
|
|
|
|
|
CONF_TYPE,
|
|
|
|
|
CONF_PORT,
|
|
|
|
|
CONF_MODEL,
|
|
|
|
|
CONF_IP_ADDRESS,
|
|
|
|
|
CONF_DEVICE_ID,
|
|
|
|
|
CONF_PROTOCOL,
|
|
|
|
|
CONF_TOKEN,
|
2023-09-09 00:14:41 +08:00
|
|
|
|
CONF_NAME,
|
|
|
|
|
CONF_DEVICE,
|
|
|
|
|
CONF_ENTITIES
|
2023-09-02 16:30:03 +08:00
|
|
|
|
)
|
2025-09-12 00:15:14 +08:00
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
from .core.logger import MideaLogger
|
2023-09-02 16:30:03 +08:00
|
|
|
|
from .core.device import MiedaDevice
|
2025-09-09 23:52:48 +08:00
|
|
|
|
from .data_coordinator import MideaDataUpdateCoordinator
|
2025-09-12 00:15:14 +08:00
|
|
|
|
from .core.cloud import get_midea_cloud
|
2023-09-02 16:30:03 +08:00
|
|
|
|
from .const import (
|
|
|
|
|
DOMAIN,
|
|
|
|
|
DEVICES,
|
2023-09-17 19:40:54 +08:00
|
|
|
|
CONF_REFRESH_INTERVAL,
|
2023-09-09 00:14:41 +08:00
|
|
|
|
CONFIG_PATH,
|
2023-09-02 16:30:03 +08:00
|
|
|
|
CONF_KEY,
|
|
|
|
|
CONF_ACCOUNT,
|
2023-09-17 19:40:54 +08:00
|
|
|
|
CONF_SN8,
|
|
|
|
|
CONF_SN,
|
|
|
|
|
CONF_MODEL_NUMBER,
|
2025-09-17 23:15:15 +08:00
|
|
|
|
CONF_SERVERS
|
2023-09-02 16:30:03 +08:00
|
|
|
|
)
|
2025-09-12 00:15:14 +08:00
|
|
|
|
# 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制)
|
|
|
|
|
from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY
|
2023-09-02 16:30:03 +08:00
|
|
|
|
|
2025-09-12 00:15:14 +08:00
|
|
|
|
PLATFORMS: list[Platform] = [
|
2023-09-09 00:14:41 +08:00
|
|
|
|
Platform.BINARY_SENSOR,
|
2025-09-24 18:20:58 +08:00
|
|
|
|
Platform.SENSOR,
|
|
|
|
|
Platform.SWITCH,
|
2023-09-09 00:14:41 +08:00
|
|
|
|
Platform.CLIMATE,
|
2023-09-10 12:08:27 +08:00
|
|
|
|
Platform.SELECT,
|
|
|
|
|
Platform.WATER_HEATER,
|
2025-09-24 23:31:31 +08:00
|
|
|
|
Platform.FAN,
|
|
|
|
|
Platform.HUMIDIFIER
|
2023-09-09 00:14:41 +08:00
|
|
|
|
]
|
|
|
|
|
|
2025-09-17 23:15:15 +08:00
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
def get_sn8_used(hass: HomeAssistant, sn8):
|
|
|
|
|
entries = hass.config_entries.async_entries(DOMAIN)
|
|
|
|
|
count = 0
|
|
|
|
|
for entry in entries:
|
|
|
|
|
if sn8 == entry.data.get("sn8"):
|
|
|
|
|
count += 1
|
|
|
|
|
return count
|
2023-09-02 16:30:03 +08:00
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
|
|
|
|
|
def remove_device_config(hass: HomeAssistant, sn8):
|
|
|
|
|
config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json")
|
|
|
|
|
try:
|
|
|
|
|
os.remove(config_file)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2025-09-17 23:15:15 +08:00
|
|
|
|
async def load_device_config(hass: HomeAssistant, device_type, sn8):
|
|
|
|
|
def _ensure_dir_and_load(path_dir: str, path_file: str):
|
|
|
|
|
os.makedirs(path_dir, exist_ok=True)
|
|
|
|
|
return load_json(path_file, default={})
|
|
|
|
|
|
|
|
|
|
config_dir = hass.config.path(CONFIG_PATH)
|
2023-09-09 00:14:41 +08:00
|
|
|
|
config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json")
|
2025-09-17 23:15:15 +08:00
|
|
|
|
raw = await hass.async_add_executor_job(_ensure_dir_and_load, config_dir, config_file)
|
|
|
|
|
json_data = {}
|
2025-09-24 18:20:58 +08:00
|
|
|
|
# if isinstance(raw, dict) and len(raw) > 0:
|
|
|
|
|
# # 兼容两种文件结构:
|
|
|
|
|
# # 1) { "<sn8>": { ...mapping... } }
|
|
|
|
|
# # 2) { ...mapping... }(直接就是映射体)
|
|
|
|
|
# if sn8 in raw:
|
|
|
|
|
# json_data = raw.get(sn8) or {}
|
|
|
|
|
# else:
|
|
|
|
|
# # 如果像映射体(包含 entities/centralized 等关键字段),直接使用
|
|
|
|
|
# if any(k in raw for k in ["entities", "centralized", "queries", "manufacturer"]):
|
|
|
|
|
# json_data = raw
|
2025-09-17 23:15:15 +08:00
|
|
|
|
if not json_data:
|
2025-09-24 16:32:23 +08:00
|
|
|
|
device_path = f".device_mapping.{'T0x%02X' % device_type}"
|
2023-09-10 12:08:27 +08:00
|
|
|
|
try:
|
2025-09-24 16:32:23 +08:00
|
|
|
|
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:
|
|
|
|
|
json_data = mapping_module.DEVICE_MAPPING["default"]
|
2025-09-24 11:41:11 +08:00
|
|
|
|
except ModuleNotFoundError:
|
2025-09-24 16:32:23 +08:00
|
|
|
|
MideaLogger.warning(f"Can't load mapping file for type {'T0x%02X' % device_type}")
|
2025-09-24 11:41:11 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
save_data = {sn8: json_data}
|
|
|
|
|
# offload save_json as well
|
|
|
|
|
await hass.async_add_executor_job(save_json, config_file, save_data)
|
2023-09-09 00:14:41 +08:00
|
|
|
|
return json_data
|
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
|
2023-09-17 19:40:54 +08:00
|
|
|
|
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
|
|
|
|
if device_id is not None:
|
|
|
|
|
ip_address = config_entry.options.get(
|
|
|
|
|
CONF_IP_ADDRESS, None
|
|
|
|
|
)
|
|
|
|
|
refresh_interval = config_entry.options.get(
|
|
|
|
|
CONF_REFRESH_INTERVAL, None
|
|
|
|
|
)
|
|
|
|
|
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
|
|
|
|
|
if device:
|
|
|
|
|
if ip_address is not None:
|
|
|
|
|
device.set_ip_address(ip_address)
|
|
|
|
|
if refresh_interval is not None:
|
|
|
|
|
device.set_refresh_interval(refresh_interval)
|
2023-09-09 00:14:41 +08:00
|
|
|
|
|
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType):
|
2023-09-02 16:30:03 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
2023-09-02 16:30:03 +08:00
|
|
|
|
device_type = config_entry.data.get(CONF_TYPE)
|
2025-09-12 00:15:14 +08:00
|
|
|
|
MideaLogger.debug(f"async_setup_entry type={device_type} data={config_entry.data}")
|
2023-09-02 16:30:03 +08:00
|
|
|
|
if device_type == CONF_ACCOUNT:
|
2025-09-12 00:15:14 +08:00
|
|
|
|
account = config_entry.data.get(CONF_ACCOUNT)
|
|
|
|
|
password = config_entry.data.get(CONF_PASSWORD_KEY)
|
|
|
|
|
server = config_entry.data.get(CONF_SERVER_KEY)
|
|
|
|
|
cloud_name = CONF_SERVERS.get(server)
|
|
|
|
|
cloud = get_midea_cloud(
|
|
|
|
|
cloud_name=cloud_name,
|
|
|
|
|
session=async_get_clientsession(hass),
|
|
|
|
|
account=account,
|
|
|
|
|
password=password,
|
|
|
|
|
)
|
|
|
|
|
if not cloud or not await cloud.login():
|
|
|
|
|
MideaLogger.error("Midea cloud login failed")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# 拉取家庭与设备列表
|
|
|
|
|
try:
|
|
|
|
|
homes = await cloud.list_home()
|
|
|
|
|
if homes and len(homes) > 0:
|
2025-09-24 16:32:23 +08:00
|
|
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
|
hass.data[DOMAIN].setdefault("accounts", {})
|
|
|
|
|
bucket = {"device_list": {}, "coordinator_map": {}}
|
|
|
|
|
home_ids = list(homes.keys())
|
2025-09-12 00:15:14 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
for home_id in home_ids:
|
|
|
|
|
appliances = await cloud.list_appliances(home_id)
|
|
|
|
|
if appliances is None:
|
|
|
|
|
continue
|
2025-09-12 00:15:14 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
# 为每台设备构建占位设备与协调器(不连接本地)
|
|
|
|
|
for appliance_code, info in appliances.items():
|
|
|
|
|
MideaLogger.debug(f"info={info} ")
|
|
|
|
|
try:
|
|
|
|
|
device = MiedaDevice(
|
|
|
|
|
name=info.get(CONF_NAME) or info.get("name"),
|
|
|
|
|
device_id=appliance_code,
|
|
|
|
|
device_type=info.get(CONF_TYPE) or info.get("type"),
|
|
|
|
|
ip_address=None,
|
|
|
|
|
port=None,
|
|
|
|
|
token=None,
|
|
|
|
|
key=None,
|
|
|
|
|
connected=info.get("online"),
|
|
|
|
|
protocol=info.get(CONF_PROTOCOL) or 2,
|
|
|
|
|
model=info.get(CONF_MODEL),
|
|
|
|
|
subtype=info.get(CONF_MODEL_NUMBER),
|
|
|
|
|
sn=info.get(CONF_SN) or info.get("sn"),
|
|
|
|
|
sn8=info.get(CONF_SN8) or info.get("sn8"),
|
|
|
|
|
)
|
|
|
|
|
# 加载并应用设备映射(queries/centralized/calculate),并预置 attributes 键
|
|
|
|
|
try:
|
|
|
|
|
mapping = await load_device_config(
|
|
|
|
|
hass,
|
|
|
|
|
info.get(CONF_TYPE) or info.get("type"),
|
|
|
|
|
info.get(CONF_SN8) or info.get("sn8"),
|
|
|
|
|
) or {}
|
|
|
|
|
except Exception:
|
|
|
|
|
mapping = {}
|
2025-09-17 22:46:38 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
try:
|
|
|
|
|
device.set_queries(mapping.get("queries", []))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
device.set_centralized(mapping.get("centralized", []))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
device.set_calculate(mapping.get("calculate", {}))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-09-17 22:46:38 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
# 预置 attributes:包含 centralized 里声明的所有键、entities 中使用到的所有属性键
|
|
|
|
|
try:
|
|
|
|
|
preset_keys = set(mapping.get("centralized", []))
|
|
|
|
|
entities_cfg = (mapping.get("entities") or {})
|
|
|
|
|
# 收集实体配置中直接引用的属性键
|
|
|
|
|
for platform_cfg in entities_cfg.values():
|
|
|
|
|
if not isinstance(platform_cfg, dict):
|
|
|
|
|
continue
|
|
|
|
|
for _, ecfg in platform_cfg.items():
|
|
|
|
|
if not isinstance(ecfg, dict):
|
|
|
|
|
continue
|
|
|
|
|
# 常见直接属性字段
|
|
|
|
|
for k in [
|
|
|
|
|
"power",
|
|
|
|
|
"aux_heat",
|
|
|
|
|
"current_temperature",
|
|
|
|
|
"target_temperature",
|
|
|
|
|
"oscillate",
|
|
|
|
|
"min_temp",
|
|
|
|
|
"max_temp",
|
|
|
|
|
]:
|
|
|
|
|
v = ecfg.get(k)
|
|
|
|
|
if isinstance(v, str):
|
|
|
|
|
preset_keys.add(v)
|
|
|
|
|
elif isinstance(v, list):
|
|
|
|
|
for vv in v:
|
|
|
|
|
if isinstance(vv, str):
|
|
|
|
|
preset_keys.add(vv)
|
|
|
|
|
# 模式映射里的条件字段
|
|
|
|
|
for map_key in [
|
|
|
|
|
"hvac_modes",
|
|
|
|
|
"preset_modes",
|
|
|
|
|
"swing_modes",
|
|
|
|
|
"fan_modes",
|
|
|
|
|
"operation_list",
|
|
|
|
|
"options",
|
|
|
|
|
]:
|
|
|
|
|
maps = ecfg.get(map_key) or {}
|
|
|
|
|
if isinstance(maps, dict):
|
|
|
|
|
for _, cond in maps.items():
|
|
|
|
|
if isinstance(cond, dict):
|
|
|
|
|
for attr_name in cond.keys():
|
|
|
|
|
preset_keys.add(attr_name)
|
|
|
|
|
# 传感器/开关等实体 key 本身也加入(其 key 即属性名)
|
|
|
|
|
for platform_name, platform_cfg in entities_cfg.items():
|
|
|
|
|
if not isinstance(platform_cfg, dict):
|
|
|
|
|
continue
|
|
|
|
|
platform_str = str(platform_name)
|
|
|
|
|
if platform_str in [
|
|
|
|
|
str(Platform.SENSOR),
|
|
|
|
|
str(Platform.BINARY_SENSOR),
|
|
|
|
|
str(Platform.SWITCH),
|
|
|
|
|
str(Platform.FAN),
|
|
|
|
|
str(Platform.SELECT),
|
|
|
|
|
]:
|
|
|
|
|
for entity_key in platform_cfg.keys():
|
|
|
|
|
preset_keys.add(entity_key)
|
|
|
|
|
# 写入默认空值
|
|
|
|
|
for k in preset_keys:
|
|
|
|
|
if k not in device.attributes:
|
|
|
|
|
device.attributes[k] = None
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-09-17 22:46:38 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device, cloud=cloud)
|
|
|
|
|
# 后台刷新,避免初始化阻塞
|
|
|
|
|
hass.async_create_task(coordinator.async_config_entry_first_refresh())
|
|
|
|
|
bucket["device_list"][appliance_code] = info
|
|
|
|
|
bucket["coordinator_map"][appliance_code] = coordinator
|
|
|
|
|
except Exception as e:
|
|
|
|
|
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
|
|
|
|
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
2025-09-12 00:15:14 +08:00
|
|
|
|
|
2025-09-24 16:32:23 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
MideaLogger.error(f"Fetch appliances failed: {e}")
|
2025-09-24 11:41:11 +08:00
|
|
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
2023-09-02 16:30:03 +08:00
|
|
|
|
return True
|
2023-09-09 00:14:41 +08:00
|
|
|
|
|
|
|
|
|
|
2023-09-10 12:08:27 +08:00
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
2023-09-09 00:14:41 +08:00
|
|
|
|
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
2025-09-12 00:15:14 +08:00
|
|
|
|
device_type = config_entry.data.get(CONF_TYPE)
|
|
|
|
|
if device_type == CONF_ACCOUNT:
|
|
|
|
|
# 卸载平台并清理账号桶
|
|
|
|
|
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
|
|
|
|
if unload_ok:
|
|
|
|
|
try:
|
|
|
|
|
hass.data.get(DOMAIN, {}).get("accounts", {}).pop(config_entry.entry_id, None)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return unload_ok
|
2023-09-09 00:14:41 +08:00
|
|
|
|
if device_id is not None:
|
2023-09-17 19:40:54 +08:00
|
|
|
|
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
|
2023-09-09 00:14:41 +08:00
|
|
|
|
if device is not None:
|
2023-09-10 12:08:27 +08:00
|
|
|
|
if get_sn8_used(hass, device.sn8) == 1:
|
|
|
|
|
remove_device_config(hass, device.sn8)
|
2025-09-12 00:15:14 +08:00
|
|
|
|
# device.close()
|
2023-09-09 00:14:41 +08:00
|
|
|
|
hass.data[DOMAIN][DEVICES].pop(device_id)
|
2025-09-17 23:15:15 +08:00
|
|
|
|
for platform in PLATFORMS:
|
2023-09-09 00:14:41 +08:00
|
|
|
|
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
|
|
|
|
return True
|