Remove file sync read warning

This commit is contained in:
sususweet
2025-09-17 23:15:15 +08:00
parent 2bfc2b9fbe
commit 4d298054f6
11 changed files with 37 additions and 238 deletions

View File

@@ -4,6 +4,7 @@ import voluptuous as vol
from importlib import import_module
from homeassistant.config_entries import ConfigEntry
from homeassistant.util.json import load_json
try:
from homeassistant.helpers.json import save_json
except ImportError:
@@ -42,7 +43,7 @@ from .const import (
CONF_SN8,
CONF_SN,
CONF_MODEL_NUMBER,
CONF_LUA_FILE, CONF_SERVERS
CONF_SERVERS
)
# 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制)
from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY
@@ -57,6 +58,7 @@ PLATFORMS: list[Platform] = [
Platform.FAN
]
def get_sn8_used(hass: HomeAssistant, sn8):
entries = hass.config_entries.async_entries(DOMAIN)
count = 0
@@ -74,13 +76,26 @@ def remove_device_config(hass: HomeAssistant, sn8):
pass
def load_device_config(hass: HomeAssistant, device_type, sn8):
os.makedirs(hass.config.path(CONFIG_PATH), exist_ok=True)
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)
config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json")
json_data = load_json(config_file, default={})
if len(json_data) > 0:
json_data = json_data.get(sn8)
else:
raw = await hass.async_add_executor_job(_ensure_dir_and_load, config_dir, config_file)
json_data = {}
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
if not json_data:
device_path = f".device_mapping.{'T0x%02X' % device_type}"
try:
mapping_module = import_module(device_path, __package__)
@@ -90,57 +105,12 @@ def load_device_config(hass: HomeAssistant, device_type, sn8):
json_data = mapping_module.DEVICE_MAPPING["default"]
if len(json_data) > 0:
save_data = {sn8: json_data}
save_json(config_file, save_data)
# offload save_json as well
await hass.async_add_executor_job(save_json, config_file, save_data)
except ModuleNotFoundError:
MideaLogger.warning(f"Can't load mapping file for type {'T0x%02X' % device_type}")
return json_data
def register_services(hass: HomeAssistant):
async def async_set_attributes(service: ServiceCall):
device_id = service.data.get("device_id")
attributes = service.data.get("attributes")
MideaLogger.debug(f"Service called: set_attributes, device_id: {device_id}, attributes: {attributes}")
try:
coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator")
except KeyError:
MideaLogger.error(f"Failed to call service set_attributes: the device {device_id} isn't exist.")
return
if coordinator:
await coordinator.async_set_attributes(attributes)
async def async_send_command(service: ServiceCall):
device_id = service.data.get("device_id")
cmd_type = service.data.get("cmd_type")
cmd_body = service.data.get("cmd_body")
try:
coordinator: MideaDataUpdateCoordinator = hass.data[DOMAIN][DEVICES][device_id].get("coordinator")
except KeyError:
MideaLogger.error(f"Failed to call service send_command: the device {device_id} isn't exist.")
return
if coordinator:
await coordinator.async_send_command(cmd_type, cmd_body)
hass.services.async_register(
DOMAIN,
"set_attributes",
async_set_attributes,
schema=vol.Schema({
vol.Required("device_id"): vol.Coerce(int),
vol.Required("attributes"): vol.Any(dict)
})
)
hass.services.async_register(
DOMAIN, "send_command", async_send_command,
schema=vol.Schema({
vol.Required("device_id"): vol.Coerce(int),
vol.Required("cmd_type"): vol.In([2, 3]),
vol.Required("cmd_body"): str
})
)
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry):
device_id = config_entry.data.get(CONF_DEVICE_ID)
if device_id is not None:
@@ -172,8 +142,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
bit_lua = base64.b64decode(BIT_LUA.encode("utf-8")).decode("utf-8")
with open(bit, "wt") as fp:
fp.write(bit_lua)
register_services(hass)
return True
@@ -234,11 +202,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
subtype=info.get(CONF_MODEL_NUMBER),
sn=info.get(CONF_SN) or info.get("sn"),
sn8=info.get(CONF_SN8) or info.get("sn8"),
lua_file=None,
)
# 加载并应用设备映射queries/centralized/calculate并预置 attributes 键
try:
mapping = load_device_config(
mapping = await load_device_config(
hass,
info.get(CONF_TYPE) or info.get("type"),
info.get(CONF_SN8) or info.get("sn8"),
@@ -352,11 +319,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
if device is not None:
if get_sn8_used(hass, device.sn8) == 1:
lua_file = config_entry.data.get("lua_file")
os.remove(lua_file)
remove_device_config(hass, device.sn8)
# device.close()
hass.data[DOMAIN][DEVICES].pop(device_id)
for platform in ALL_PLATFORM:
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
return True

View File

@@ -29,7 +29,7 @@ async def async_setup_entry(
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.BINARY_SENSOR, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")

View File

@@ -37,7 +37,7 @@ async def async_setup_entry(
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.CLIMATE, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")

View File

@@ -96,7 +96,7 @@ class MideaCloud:
break
except Exception as e:
pass
print(response)
if int(response["code"]) == 0 and "data" in response:
return response["data"]

View File

@@ -1,10 +1,8 @@
import threading
import socket
import time
from enum import IntEnum
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
from .packet_builder import PacketBuilder
from .lua_runtime import MideaCodec
from .message import MessageQuestCustom
from .logger import MideaLogger
@@ -41,8 +39,7 @@ class MiedaDevice(threading.Thread):
subtype: int | None,
connected: bool,
sn: str | None,
sn8: str | None,
lua_file: str | None):
sn8: str | None):
threading.Thread.__init__(self)
self._socket = None
self._ip_address = ip_address
@@ -74,7 +71,6 @@ class MiedaDevice(threading.Thread):
self._centralized = []
self._calculate_get = []
self._calculate_set = []
self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None
@property
def device_name(self):
@@ -136,8 +132,6 @@ class MiedaDevice(threading.Thread):
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
new_status[attribute] = value
if set_cmd := self._lua_runtime.build_control(new_status):
self._build_send(set_cmd)
def set_attributes(self, attributes):
new_status = {}
@@ -148,9 +142,6 @@ class MiedaDevice(threading.Thread):
if attribute in self._attributes.keys():
has_new = True
new_status[attribute] = value
if has_new:
if set_cmd := self._lua_runtime.build_control(new_status):
self._build_send(set_cmd)
def set_ip_address(self, ip_address):
MideaLogger.debug(f"Update IP address to {ip_address}")
@@ -219,72 +210,6 @@ class MiedaDevice(threading.Thread):
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
self._send_message(msg)
def _refresh_status(self):
for query in self._queries:
if query_cmd := self._lua_runtime.build_query(query):
self._build_send(query_cmd)
def _parse_message(self, msg):
if self._protocol == 3:
messages, self._buffer = self._security.decode_8370(self._buffer + msg)
else:
messages, self._buffer = self.fetch_v2_message(self._buffer + msg)
if len(messages) == 0:
return ParseMessageResult.PADDING
for message in messages:
if message == b"ERROR":
return ParseMessageResult.ERROR
payload_len = message[4] + (message[5] << 8) - 56
payload_type = message[2] + (message[3] << 8)
if payload_type in [0x1001, 0x0001]:
# Heartbeat detected
pass
elif len(message) > 56:
cryptographic = message[40:-16]
if payload_len % 16 == 0:
decrypted = self._security.aes_decrypt(cryptographic)
MideaLogger.debug(f"Received: {decrypted.hex().lower()}")
if status := self._lua_runtime.decode_status(decrypted.hex()):
MideaLogger.debug(f"Decoded: {status}")
new_status = {}
for single in status.keys():
value = status.get(single)
if single not in self._attributes or self._attributes[single] != value:
self._attributes[single] = value
new_status[single] = value
if len(new_status) > 0:
for c in self._calculate_get:
lvalue = c.get("lvalue")
rvalue = c.get("rvalue")
if lvalue and rvalue:
calculate = False
for s, v in new_status.items():
if rvalue.find(f"[{s}]") >= 0:
calculate = True
break
if calculate:
calculate_str1 = \
(f"{lvalue.replace('[', 'self._attributes[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[","[\"").replace("]","\"]")
calculate_str2 = \
(f"{lvalue.replace('[', 'new_status[')} = "
f"{rvalue.replace('[', 'self._attributes[')}") \
.replace("[","[\"").replace("]","\"]")
try:
exec(calculate_str1)
exec(calculate_str2)
except Exception:
MideaLogger.warning(
f"Calculation Error: {lvalue} = {rvalue}", self._device_id
)
self._update_all(new_status)
return ParseMessageResult.SUCCESS
def _send_heartbeat(self):
msg = PacketBuilder(self._device_id, bytearray([0x00])).finalize(msg_type=0)
self._send_message(msg)
def _device_connected(self, connected=True):
self._connected = connected
status = {"connected": connected}

View File

@@ -1,91 +0,0 @@
import lupa
import threading
import json
from .logger import MideaLogger
class LuaRuntime:
def __init__(self, file):
self._runtimes = lupa.LuaRuntime()
string = f'dofile("{file}")'
self._runtimes.execute(string)
self._lock = threading.Lock()
self._json_to_data = self._runtimes.eval("function(param) return jsonToData(param) end")
self._data_to_json = self._runtimes.eval("function(param) return dataToJson(param) end")
def json_to_data(self, json_value):
with self._lock:
result = self._json_to_data(json_value)
return result
def data_to_json(self, data_value):
with self._lock:
result = self._data_to_json(data_value)
return result
class MideaCodec(LuaRuntime):
def __init__(self, file, sn=None, subtype=None):
super().__init__(file)
self._sn = sn
self._subtype = subtype
def _build_base_dict(self):
device_info ={}
if self._sn is not None:
device_info["deviceSN"] = self._sn
if self._subtype is not None:
device_info["deviceSubType"] = self._subtype
base_dict = {
"deviceinfo": device_info
}
return base_dict
def build_query(self, append=None):
query_dict = self._build_base_dict()
query_dict["query"] = {} if append is None else append
json_str = json.dumps(query_dict)
try:
result = self.json_to_data(json_str)
return result
except lupa.LuaError as e:
MideaLogger.error(f"LuaRuntimeError in build_query {json_str}: {repr(e)}")
return None
def build_control(self, append=None):
query_dict = self._build_base_dict()
query_dict["control"] = {} if append is None else append
json_str = json.dumps(query_dict)
try:
result = self.json_to_data(json_str)
return result
except lupa.LuaError as e:
MideaLogger.error(f"LuaRuntimeError in build_control {json_str}: {repr(e)}")
return None
def build_status(self, append=None):
query_dict = self._build_base_dict()
query_dict["status"] = {} if append is None else append
json_str = json.dumps(query_dict)
try:
result = self.json_to_data(json_str)
return result
except lupa.LuaError as e:
MideaLogger.error(f"LuaRuntimeError in build_status {json_str}: {repr(e)}")
return None
def decode_status(self, data: str):
data_dict = self._build_base_dict()
data_dict["msg"] = {
"data": data
}
json_str = json.dumps(data_dict)
try:
result = self.data_to_json(json_str)
status = json.loads(result)
return status.get("status")
except lupa.LuaError as e:
MideaLogger.error(f"LuaRuntimeError in decode_status {data}: {repr(e)}")
return None

View File

@@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.FAN, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")

View File

@@ -6,6 +6,6 @@
"documentation": "https://github.com/sususweet/midea-auto-codec#readme",
"iot_class": "cloud_push",
"issue_tracker": "https://github.com/sususweet/midea-auto-codec/issues",
"requirements": ["lupa>=2.0"],
"version": "v0.0.3"
"requirements": [],
"version": "v0.0.5"
}

View File

@@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.SELECT, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")

View File

@@ -26,7 +26,7 @@ async def async_setup_entry(
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.SENSOR, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")

View File

@@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for device_id, info in device_list.items():
device_type = info.get("type")
sn8 = info.get("sn8")
config = load_device_config(hass, device_type, sn8) or {}
config = await load_device_config(hass, device_type, sn8) or {}
entities_cfg = (config.get("entities") or {}).get(Platform.WATER_HEATER, {})
manufacturer = config.get("manufacturer")
rationale = config.get("rationale")