Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
636 changes: 543 additions & 93 deletions custom_components/meiju/__init__.py

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions custom_components/meiju/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Config flow for Meiju."""

from __future__ import annotations

import logging
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.helpers import config_validation as cv

DOMAIN = "meiju"

_LOGGER = logging.getLogger(__name__)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1

async def async_step_user(self, user_input=None):
errors = {}
if user_input is not None:
from .core.cloud import MeijuCloud
from msmart.cloud import MSmartCloud as MsmartCloud
try:
server = user_input.get("cloud_server", "china")
username = user_input["username"]
password = user_input["password"]
if server == "china":
cloud = MeijuCloud(username, password)
else:
cloud = MsmartCloud(username, password)
await self.hass.async_add_executor_job(cloud.login)
if not cloud.accessToken:
errors["base"] = "auth"
else:
title = username
return self.async_create_entry(title=title, data=user_input)
except Exception as exc:
_LOGGER.warning("Meiju login failed: %s", exc)
errors["base"] = "auth"

schema = vol.Schema(
{
vol.Required("username"): cv.string,
vol.Required("password"): cv.string,
vol.Optional("cloud_server", default="china"): cv.string,
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
80 changes: 79 additions & 1 deletion custom_components/meiju/core/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,19 @@ class MeijuCloud(MsmartCloud):
LOGIN_KEY = 'ad0ee21d48a64bf49f4fb583ab76e799'

def __init__(self, username, password):
super().__init__(username, password)
# 必须设置use_china_server=True,使用国内API端点
super().__init__(username, password, use_china_server=True)
self.security = MsmartSecurity(use_china_server=True)
self.device_id = f'86{int(time.time() * 1000)}'

def get_login_id(self):
"""Override to use correct v5 API endpoint"""
response = self.api_request(
'/v1/user/login/id/get',
{'loginAccount': self.login_account}
)
self.login_id = response['loginId']

def login(self):
if self.session:
return
Expand All @@ -57,3 +66,72 @@ def login(self):
self.accessToken = self.session.get('mdata', {}).get('accessToken')
if not self.accessToken:
_LOGGER.warning('Login failed: %s', [self.login_account, dat, self.session])

def list_homegroups(self, force_update=False):
"""Override to use correct v5 API endpoint"""
if not self.home_groups or force_update:
response = self.api_request('/v1/homegroup/list/get', {})
self.home_groups = response.get('homeList', [])
return self.home_groups

def list(self, home_group_id=-1):
"""Override to use correct device list API"""
# Use the /v1/user/device/list endpoint which returns all devices
response = self.api_request('/v1/user/device/list', {})
device_list = response.get('homeList', [])
# Filter by home_group_id if specified
if home_group_id != -1:
device_list = [d for d in device_list if d.get('homegroupId') == str(home_group_id)]
self.appliance_list = device_list
return self.appliance_list

def lua_control(self, appliance_code, command):
"""Control device via Lua control API"""
response = self.api_request('/mjl/v1/device/lua/control', {
'applianceCode': str(appliance_code),
'command': {'control': command},
})
return response

def lua_status(self, appliance_code):
"""Query device status via Lua status API"""
response = self.api_request('/mjl/v1/device/status/lua/get', {
'applianceCode': str(appliance_code),
'command': {'query': {}},
})
return response

def appliance_transparent_send(self, id, data):
"""Override to use correct v5 API endpoint and format"""
import base64

if not self.session:
self.login()

_LOGGER.debug("Sending to {}: {}".format(id, data.hex()))

# 编码命令数据为逗号分隔的字符串,然后AES加密
encoded = self.encode(data)
encrypted = self.security.aes_encrypt(encoded)

# 使用base64编码而不是hex
order = base64.b64encode(encrypted).decode('utf-8')

response = self.api_request('/appliance/transparent/send', {
'applianceCode': str(id),
'order': order,
'timestamp': 'true',
'funcId': '0',
'cmdType': '0',
})

# 解码响应
if 'reply' in response:
reply_b64 = response['reply']
reply_bytes = base64.b64decode(reply_b64)
reply = self.decode(self.security.aes_decrypt(reply_bytes))
_LOGGER.debug("Received from {}: {}".format(id, reply.hex()))
return reply
else:
_LOGGER.warning("No reply in response: {}".format(response))
return bytearray()
143 changes: 143 additions & 0 deletions custom_components/meiju/device_customizes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,146 @@ EA:
status:
byte: 18
dict: [idle, schedule, cooking, keep_warn]

# 空调
AC:
get_extra: {0x41: 0}
set_extra: {10: 0x40}
sensors:
indoor_temperature:
thing_model_property: centralAirConditioner.indoorTemperature
byte: 21
unit: °C
outdoor_temperature:
thing_model_property: centralAirConditioner.outdoorTemperature
byte: 22
unit: °C
switches:
power:
thing_model_property: centralAirConditioner.power
byte: 11
state_template: '{{ value|bitwise_and(0x01) > 0 }}'
on_extra: {11: 0x01}
off_extra: {11: 0x00}
on_lua: {power: "on"}
off_lua: {power: "off"}
selects:
mode:
byte: 12
options:
1: {name: 自动, extra: {12: 0x20, 11: 0x01}, lua: {mode: auto}}
2: {name: 制冷, extra: {12: 0x40, 11: 0x01}, lua: {mode: cool}}
3: {name: 除湿, extra: {12: 0x60, 11: 0x01}, lua: {mode: dry}}
4: {name: 制热, extra: {12: 0x80, 11: 0x01}, lua: {mode: heat}}
5: {name: 送风, extra: {12: 0xA0, 11: 0x01}, lua: {mode: fan}}
state_template: '{{ ((value or 0) // 32)|bitwise_and(0x07) }}'
set_extra: {11: 0x01}
fan_speed:
thing_model_property: centralAirConditioner.windSpeedGear
byte: 13
options:
102: {name: 自动, lua: {wind_speed: 102}}
40: {name: 低风, lua: {wind_speed: 40}}
60: {name: 中风, lua: {wind_speed: 60}}
80: {name: 高风, lua: {wind_speed: 80}}
20: {name: 静音, lua: {wind_speed: 20}}
swing_mode:
byte: 17
options:
0: {name: 关闭, extra: {17: 0x30}, lua: {swing: off}}
12: {name: 上下扫风, extra: {17: 0x3C}, lua: {swing: vertical}}
3: {name: 左右扫风, extra: {17: 0x33}, lua: {swing: horizontal}}
15: {name: 上下左右, extra: {17: 0x3F}, lua: {swing: both}}
state_template: '{{ (value or 0)|bitwise_and(0x0F) }}'
set_extra: {17: 0x30}
numbers:
target_temperature:
byte: 12
step: 0.5
min: 16
max: 30
unit: °C
state_template: '{{ ((value or 0)|bitwise_and(0x0F)) + 16 + (((value or 0) // 16)|bitwise_and(0x01) * 0.5) }}'
set_extra: {11: 0x01}
lua_key: temperature

# 净水机/软水机
ED:
get_extra: {10: 0x01, 11: 0x01}
sensors:
in_tds:
thing_model_property: [waterPurifier.influentTds, waterPurificationEquipment.in_tds]
unit: ppm
out_tds:
thing_model_property: [waterPurifier.effluentTds, waterPurificationEquipment.out_tds]
unit: ppm
water_consumption:
thing_model_property: [waterPurificationEquipment.todaySoftWaterConsumption, waterPurifier.todayPurifiedVolume]
unit: L
life1:
thing_model_property: [waterPurificationEquipment.softenerSaltRemaining, waterPurificationEquipment.filter1RemainingLife]
unit: '%'
life2:
thing_model_property: [waterPurificationEquipment.softenerSaltAmount, waterPurificationEquipment.filter2RemainingLife]
unit: '%'
life3:
thing_model_property: waterPurificationEquipment.waterHardness
unit: ''
switches:
power:
thing_model_property: [waterSoftener.waterSoftenSwitch, waterPurificationEquipment.power]

# 洗碗机
E1:
get_extra: {10: 0x00}
sensors:
status:
thing_model_property: dishwasher.workStatus
dict: {0: off, 1: idle, 2: delay, 3: running, 4: storage, 5: error}
mode:
thing_model_property: dishwasher.workMode
progress:
thing_model_property: dishwasher.currentWashStage
dict: {0: idle, 1: pre_wash, 2: wash, 3: rinse, 4: dry, 5: complete}
time_remaining:
thing_model_property: dishwasher.remainingRunningTime
unit: min
temperature:
thing_model_property: dishwasher.currentWashingTemperature
unit: °C
error_code:
thing_model_property: dishwasher.errorCode
binary_sensors:
door:
thing_model_property: dishwasher.doorState
state_map: {0: closed, 1: open}
rinse_aid_shortage:
thing_model_property: dishwasher.rinseAidLowState
salt_shortage:
thing_model_property: dishwasher.softenerSaltLowState
water_lack:
thing_model_property: dishwasher.waterLackState
switches:
power:
thing_model_property: dishwasher.power

# 冰箱
CA:
get_extra: {10: 0x00}
sensors:
fridge_temp:
byte: 17
unit: °C
freezer_temp:
byte: 18
unit: °C
fridge_setting_temp:
byte: 2
unit: °C
freezer_setting_temp:
byte: 2
unit: °C
switches:
power:
byte: 6
state_template: '{{ value|bitwise_and(0x01) == 0 }}'
7 changes: 4 additions & 3 deletions custom_components/meiju/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@


async def async_setup_entry(hass, config_entry, async_add_entities):
cfg = {**config_entry.data, **config_entry.options}
await async_setup_platform(hass, cfg, async_setup_platform, async_add_entities)
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities


async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None):
Expand Down Expand Up @@ -59,4 +58,6 @@ async def async_set_value(self, value: float):

async def async_set_native_value(self, value: float):
"""Set new value."""
return await self.async_set_property(value)
lua_key = self._option.get('lua_key')
lua_cmd = {lua_key: value} if lua_key else None
return await self.async_set_property(value, lua_command=lua_cmd)
13 changes: 9 additions & 4 deletions custom_components/meiju/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@


async def async_setup_entry(hass, config_entry, async_add_entities):
cfg = {**config_entry.data, **config_entry.options}
await async_setup_platform(hass, cfg, async_setup_platform, async_add_entities)
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities


async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None):
Expand All @@ -44,16 +43,22 @@ def __init__(self, name, device: BaseDevice, option=None):

async def async_update(self):
await super().async_update()
self._attr_current_option = self._options.get(self._attr_state, self._attr_state)
opt = self._options.get(self._attr_state, self._attr_state)
if isinstance(opt, dict):
opt = opt.get('name', str(self._attr_state))
self._attr_current_option = opt

async def async_select_option(self, option: str):
"""Change the selected option."""
extra = {}
lua = {}
if isinstance(self._options, dict):
for k, v in self._options.items():
opt = v if isinstance(v, dict) else {'name': v}
if option == opt.get('name', k):
option = k
extra = opt.get('extra') or {}
lua = opt.get('lua') or {}
break
return await self.async_set_property(option, extra)
lua_cmd = lua if lua else None
return await self.async_set_property(option, extra, lua_command=lua_cmd)
17 changes: 17 additions & 0 deletions custom_components/meiju/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"config": {
"step": {
"user": {
"title": "Midea Meiju Account",
"data": {
"username": "Username",
"password": "Password",
"cloud_server": "Cloud Server"
}
}
},
"error": {
"auth": "Login failed. Please check your credentials."
}
}
}
8 changes: 5 additions & 3 deletions custom_components/meiju/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@


async def async_setup_entry(hass, config_entry, async_add_entities):
cfg = {**config_entry.data, **config_entry.options}
await async_setup_platform(hass, cfg, async_setup_platform, async_add_entities)
hass.data[DOMAIN]['add_entities'][ENTITY_DOMAIN] = async_add_entities


async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None):
Expand All @@ -44,10 +43,13 @@ async def async_turn_switch(self, on=True, **kwargs):
if on:
val = self._option.get('on_value', 0x01)
ext = self._option.get('on_extra', {})
lua = self._option.get('on_lua')
else:
val = self._option.get('off_value', 0x00)
ext = self._option.get('off_extra', {})
ret = await self.async_set_property(val, ext)
lua = self._option.get('off_lua')
lua_cmd = lua if lua else None
ret = await self.async_set_property(val, ext, lua_command=lua_cmd)
if ret:
self._attr_is_on = not not on
self.async_write_ha_state()
Expand Down