Skip to content

Commit

Permalink
v0.3.21 pre-release
Browse files Browse the repository at this point in the history
  • Loading branch information
georgezhao2010 committed Sep 20, 2023
1 parent df8ef3b commit dc12eed
Show file tree
Hide file tree
Showing 64 changed files with 1,254 additions and 262 deletions.
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
sensitive/

#exclude
custom_components/midea_ac_lan/midea/devices/bc/
custom_components/midea_ac_lan/midea/devices/b2/
custom_components/midea_ac_lan/midea/devices/b4/
custom_components/midea_ac_lan/midea/devices/b9/
Wcustom_components/midea_ac_lan/midea/devices/b7/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ And more.
| AC | Air Conditioner | [AC.md](doc/AC.md) |
| B0 | Microwave Oven | [B0.md](doc/B0.md) |
| B1 | Electric Oven | [B1.md](doc/B1.md) |
| B3 | Dish Sterilizer | [B3.md](doc/B3.md) |
| B4 | Toaster | [B4.md](doc/B4.md) |
| B6 | Range Hood | [B6.md](doc/B6.md) |
| BF | Microwave Steam Oven | [BF.md](doc/BF.md) |
| C2 | Toilet | [C2.md](doc/C2.md) |
Expand All @@ -53,6 +55,7 @@ And more.
| E2 | Electric Water Heater | [E2.md](doc/E2.md) |
| E3 | Gas Water Heater | [E3.md](doc/E3.md) |
| E6 | Gas Stove | [E6.md](doc/E6.md) |
| E8 | Electric Slow Cooker | [E8.md](doc/E8.md) |
| EA | Electric Rice Cooker | [EA.md](doc/EA.md) |
| EC | Electric Pressure Cooker | [EC.md](doc/EC.md) |
| ED | Water Drinking Appliance | [ED.md](doc/ED.md) |
Expand All @@ -73,6 +76,8 @@ After installation, search and add component Midea AC LAN in Home Assistant inte

Or click [![Configuration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=midea_ac_lan)

***Note: During the configuration process, you may be asked to enter your Midea account and password. It's necessary to retrieve appliance information (Token and Key) from Midea cloud server. After all appliances configured, you can remove the Midea account configuration without affecting the use of the appliance.***

## Automatically
This component could auto-discover and list Midea M-Smart appliances in network, select one and add it in. You could repeat the above action to add multiple devices.

Expand Down
5 changes: 5 additions & 0 deletions README_hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
| AC | 空调器 | [AC_hans.md](doc/AC_hans.md) |
| B0 | 微波炉 | [B0_hans.md](doc/B0_hans.md) |
| B1 | 电烤箱 | [B1_hans.md](doc/B1_hans.md) |
| B3 | 消毒碗柜 | [B3_hans.md](doc/B3_hans.md) |
| B4 | 小烤箱 | [B4_hans.md](doc/B4_hans.md) |
| B6 | 油烟机 | [B6_hans.md](doc/B6_hans.md) |
| BF | 微蒸烤一体机 | [BF_hans.md](doc/BF_hans.md) |
| C2 | 智能马桶 | [C2_hans.md](doc/C2_hans.md) |
Expand All @@ -53,6 +55,7 @@
| E2 | 电热水器 | [E2_hans.md](doc/E2_hans.md) |
| E3 | 燃气热水器 | [E3_hans.md](doc/E3_hans.md) |
| E6 | 壁挂炉 | [E6_hans.md](doc/E6_hans.md) |
| E8 | 慢炖锅 | [E8_hans.md](doc/E8_hans.md) |
| EA | 电饭煲 | [EA_hans.md](doc/EA_hans.md) |
| EC | 电压力锅 | [EC_hans.md](doc/EC_hans.md) |
| ED | 饮用水设备 | [ED_hans.md](doc/ED_hans.md) |
Expand All @@ -73,6 +76,8 @@

或者直接点击 [![Configuration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=midea_ac_lan)

***注意: 配置过程中,可能会要求输入你的美的账号及密码,这是因为需要去美的云服务器获取设备的信息 (Token and Key)。在完成所有设备配置后,可以删除美的账户配置,不影响设备的使用***

## 自动
自动配置会自动搜索网络上的美的M-Smart设备,并将搜索到的设备列出,选择一个设备执行添加。多台设备执行多次添加即可。

Expand Down
27 changes: 4 additions & 23 deletions custom_components/midea_ac_lan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import homeassistant.helpers.config_validation as cv
from .const import (
DOMAIN,
CONF_ACCOUNT,
CONF_KEY,
CONF_MODEL,
CONF_REFRESH_INTERVAL,
Expand Down Expand Up @@ -63,15 +64,6 @@ async def async_setup(hass: HomeAssistant, hass_config: dict):
if attribute.get("type") in EXTRA_SWITCH and attribute_name.value not in attributes:
attributes.append(attribute_name.value)

def service_set_ac_fan_speed(service):
device_id = service.data.get("device_id")
fan_speed = service.data.get("fan_speed")
if fan_speed == "auto":
fan_speed = 102
dev = hass.data[DOMAIN][DEVICES].get(device_id)
if dev and dev.device_type == 0xac:
dev.set_attribute(attr="fan_speed", value=fan_speed)

def service_set_attribute(service):
device_id = service.data.get("device_id")
attr = service.data.get("attribute")
Expand Down Expand Up @@ -100,19 +92,6 @@ def service_send_command(service):
if dev:
dev.send_command(cmd_type, cmd_body)

hass.services.async_register(
DOMAIN,
"set_ac_fan_speed",
service_set_ac_fan_speed,
schema=vol.Schema(
{
vol.Required("device_id"): vol.Coerce(int),
vol.Required("fan_speed"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=1, max=100)),
vol.All(str, vol.In(["auto"])))
}
)
)

hass.services.async_register(
DOMAIN,
"set_attribute",
Expand Down Expand Up @@ -142,11 +121,13 @@ def service_send_command(service):


async def async_setup_entry(hass: HomeAssistant, config_entry):
device_type = config_entry.data.get(CONF_TYPE)
if device_type == CONF_ACCOUNT:
return True
name = config_entry.data.get(CONF_NAME)
device_id = config_entry.data.get(CONF_DEVICE_ID)
if name is None:
name = f"{device_id}"
device_type = config_entry.data.get(CONF_TYPE)
if device_type is None:
device_type = 0xac
token = config_entry.data.get(CONF_TOKEN)
Expand Down
143 changes: 72 additions & 71 deletions custom_components/midea_ac_lan/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
DOMAIN,
EXTRA_SENSOR,
EXTRA_CONTROL,
CONF_ACCOUNT,
CONF_SERVER,
CONF_KEY,
CONF_MODEL,
CONF_REFRESH_INTERVAL,
MIDEA_DEFAULT_ACCOUNT,
MIDEA_DEFAULT_PASSWORD,
MIDEA_DEFAULT_SERVER
CONF_REFRESH_INTERVAL
)
from homeassistant import config_entries
from homeassistant.core import callback
Expand All @@ -23,20 +22,15 @@
CONF_PORT,
CONF_SWITCHES,
CONF_SENSORS,
CONF_CUSTOMIZE
CONF_CUSTOMIZE,
CONF_PASSWORD,
)
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.util.json import load_json
try:
from homeassistant.helpers.json import save_json
except ImportError:
from homeassistant.util.json import save_json
from .midea.core.discover import discover
from .midea.core.cloud import MeijuCloud, MSmartHomeCloud
from .midea.core.cloud import get_midea_cloud
from .midea.core.device import MiedaDevice
from .midea_devices import MIDEA_DEVICES
import voluptuous as vol
import os
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)
Expand All @@ -45,25 +39,16 @@
PROTOCOLS = {1: "V1", 2: "V2", 3: "V3"}
STORAGE_PATH = f".storage/{DOMAIN}"


def save_device_token(hass, device_id, toekn, key):
os.makedirs(hass.config.path(STORAGE_PATH), exist_ok=True)
record_file = hass.config.path(f"{STORAGE_PATH}/{device_id}.json")
json_data = {"token": toekn, "key": key}
save_json(record_file, json_data)


def load_device_token(hass, device_id):
os.makedirs(hass.config.path(STORAGE_PATH), exist_ok=True)
record_file = hass.config.path(f"{STORAGE_PATH}/{device_id}.json")
json_data = load_json(record_file, default={})
if len(json_data) > 0:
return json_data.get("token"), json_data.get("key")
return None, None
servers = {
1: "MSmartHome",
2: "美的美居",
}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_session = None
_account = None
_password = None
_server = None
available_device = []
devices = {}
found_device = {}
Expand All @@ -76,6 +61,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for item in unsorted:
supports[item[0]] = item[1]

def _get_configured_account(self):
for entry in self._async_current_entries():
if entry.data.get(CONF_TYPE) == CONF_ACCOUNT:
password = bytes.fromhex(format((
int(entry.data.get(CONF_PASSWORD), 16) ^
int(entry.data.get(CONF_ACCOUNT).encode("utf-8").hex(), 16)
), 'X')).decode('UTF-8')
return entry.data.get(CONF_ACCOUNT), password, servers[entry.data.get(CONF_SERVER)]
return None, None, None

def _already_configured(self, device_id, ip_address):
for entry in self._async_current_entries():
if device_id == entry.data.get(CONF_DEVICE_ID) or ip_address == entry.data.get(CONF_IP_ADDRESS):
Expand All @@ -101,7 +96,44 @@ async def async_step_user(self, user_input=None, error=None):
errors={"base": error} if error else None
)

async def async_step_login(self, user_input=None, error=None):
if user_input is not None:
session = async_create_clientsession(self.hass)
cloud = get_midea_cloud(
session=session,
cloud_name=servers[user_input[CONF_SERVER]],
account=user_input[CONF_ACCOUNT],
password=user_input[CONF_PASSWORD]
)
_LOGGER.debug(
f"account = {user_input[CONF_ACCOUNT]}, password = {user_input[CONF_PASSWORD]}, server = {servers[user_input[CONF_SERVER]]}")
if await cloud.login():
password = format((int(user_input[CONF_ACCOUNT].encode("utf-8").hex(), 16) ^
int(user_input[CONF_PASSWORD].encode("utf-8").hex(), 16)), 'x')
return self.async_create_entry(
title=f"{user_input[CONF_ACCOUNT]}",
data={
CONF_TYPE: CONF_ACCOUNT,
CONF_ACCOUNT: user_input[CONF_ACCOUNT],
CONF_PASSWORD: password,
CONF_SERVER: user_input[CONF_SERVER]
})
else:
return await self.async_step_login(error="login_failed")
return self.async_show_form(
step_id="login",
data_schema=vol.Schema({
vol.Required(CONF_ACCOUNT): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_SERVER, default=1): vol.In(servers)
}),
errors={"base": error} if error else None
)

async def async_step_discover(self, user_input=None, error=None):
self._account, self._password, self._server = self._get_configured_account()
if self._account is None:
return await self.async_step_login()
if user_input is not None:
self.devices = discover(self.supports.keys())
self.available_device = {}
Expand Down Expand Up @@ -136,6 +168,9 @@ async def async_step_list(self, user_input=None, error=None):
)

async def async_step_byip(self, user_input=None, error=None):
self._account, self._password, self._server = self._get_configured_account()
if self._account is None:
return await self.async_step_login()
if user_input is not None:
self.devices = discover(self.supports.keys(), ip_address=user_input[CONF_IP_ADDRESS])
self.available_device = {}
Expand All @@ -160,42 +195,9 @@ async def async_step_auto(self, user_input=None, error=None):
device_id = user_input[CONF_DEVICE]
device = self.devices.get(device_id)
if device.get(CONF_PROTOCOL) == 3:
saved_token, saved_key = load_device_token(self.hass, device_id)
if saved_token is not None and saved_key is not None:
_LOGGER.debug(f"Try to using saved token and key, token: {saved_token}, key: {saved_key}")
dm = MiedaDevice(
name="",
device_id=device_id,
device_type=device.get(CONF_TYPE),
ip_address=device.get(CONF_IP_ADDRESS),
port=device.get(CONF_PORT),
token=saved_token,
key=saved_key,
protocol=3,
model=device.get(CONF_MODEL),
attributes={}
)
if dm.connect(refresh_status=False):
self.found_device = {
CONF_DEVICE_ID: device_id,
CONF_TYPE: device.get(CONF_TYPE),
CONF_PROTOCOL: 3,
CONF_IP_ADDRESS: device.get(CONF_IP_ADDRESS),
CONF_PORT: device.get(CONF_PORT),
CONF_MODEL: device.get(CONF_MODEL),
CONF_TOKEN: saved_token,
CONF_KEY: saved_key,
}
dm.close_socket()
return await self.async_step_manual()
if self._session is None:
self._session = async_create_clientsession(self.hass)
if MIDEA_DEFAULT_SERVER == "美居":
cloud = MeijuCloud(self._session, MIDEA_DEFAULT_ACCOUNT, MIDEA_DEFAULT_PASSWORD)
elif MIDEA_DEFAULT_SERVER == "MSmartHome":
cloud = MSmartHomeCloud(self._session, MIDEA_DEFAULT_ACCOUNT, MIDEA_DEFAULT_PASSWORD)
else:
return await self.async_step_auto(error="cant_get_token")
session = async_create_clientsession(self.hass)
_LOGGER.debug(f"server = {self._server}, account = {self._account}, password = {self._password}")
cloud = get_midea_cloud(self._server, session, self._account, self._password)
if await cloud.login():
keys = await cloud.get_keys(user_input[CONF_DEVICE])
for method, key in keys.items():
Expand All @@ -212,7 +214,7 @@ async def async_step_auto(self, user_input=None, error=None):
attributes={}
)
_LOGGER.debug(f"Successful to take token and key, token: {key['token']},"
f" key: { key['key']}, method: {method}")
f" key: {key['key']}, method: {method}")
if dm.connect(refresh_status=False):
self.found_device = {
CONF_DEVICE_ID: device_id,
Expand All @@ -227,7 +229,7 @@ async def async_step_auto(self, user_input=None, error=None):
dm.close_socket()
return await self.async_step_manual()
return await self.async_step_auto(error="connect_error")
return await self.async_step_auto(error="cant_get_token")
return await self.async_step_auto(error="login_failed")
else:
self.found_device = {
CONF_DEVICE_ID: device_id,
Expand Down Expand Up @@ -280,7 +282,6 @@ async def async_step_manual(self, user_input=None, error=None):
)
if dm.connect(refresh_status=False):
dm.close_socket()
save_device_token(self.hass, user_input[CONF_DEVICE_ID], user_input[CONF_TOKEN], user_input[CONF_KEY])
return self.async_create_entry(
title=f"{user_input[CONF_NAME]}",
data={
Expand Down Expand Up @@ -406,25 +407,25 @@ async def async_step_init(self, user_input=None):
CONF_SENSORS,
default=extra_sensors,
):
cv.multi_select(sensors)
cv.multi_select(sensors)
})
if len(switches) > 0:
data_schema = data_schema.extend({
vol.Required(
CONF_SWITCHES,
default=extra_switches,
):
cv.multi_select(switches)
cv.multi_select(switches)
})
data_schema = data_schema.extend({
vol.Optional(
CONF_CUSTOMIZE,
default=customize,
):
str
str
})

return self.async_show_form(
step_id="init",
data_schema=data_schema
)
)
11 changes: 2 additions & 9 deletions custom_components/midea_ac_lan/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,11 @@
DEVICES = "devices"
CONF_KEY = "key"
CONF_MODEL = "model"
CONF_ACCOUNT= "account"
CONF_SERVER = "server"
CONF_REFRESH_INTERVAL = "refresh_interval"
EXTRA_SENSOR = [Platform.SENSOR, Platform.BINARY_SENSOR]
EXTRA_SWITCH = [Platform.SWITCH, Platform.LOCK, Platform.SELECT, Platform.NUMBER]
EXTRA_CONTROL = [Platform.CLIMATE, Platform.WATER_HEATER, Platform.FAN, Platform.HUMIDIFIER, Platform.LIGHT] + \
EXTRA_SWITCH
ALL_PLATFORM = EXTRA_SENSOR + EXTRA_CONTROL
MIDEA_DEFAULT_ACCOUNT = bytes.fromhex(format((
23463126243109184681794222863140941720816703778507241760732920879 ^
39182118275972017797890111985649342047468653967530949796945843010
), 'X')).decode('UTF-8')
MIDEA_DEFAULT_PASSWORD = bytes.fromhex(format((
52060369457599433878037326532120919 ^
556568442326836838504752433645123377
), 'X')).decode('UTF-8')
MIDEA_DEFAULT_SERVER = "MSmartHome" # MSmartHome/美居
Loading

0 comments on commit dc12eed

Please sign in to comment.