This commit is contained in:
unknown
2023-09-09 00:14:41 +08:00
parent 34c8e06cd8
commit 10d127c940
17 changed files with 690 additions and 125 deletions

View File

@@ -1,6 +1,11 @@
import logging import logging
import os import os
import base64 import base64
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 homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.const import ( from homeassistant.const import (
Platform, Platform,
@@ -11,19 +16,53 @@ from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_PROTOCOL, CONF_PROTOCOL,
CONF_TOKEN, CONF_TOKEN,
CONF_NAME CONF_NAME,
CONF_DEVICE,
CONF_ENTITIES
) )
from .device_map.device_mapping import DEVICE_MAPPING
from .core.device import MiedaDevice from .core.device import MiedaDevice
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES, DEVICES,
CONFIG_PATH,
CONF_KEY, CONF_KEY,
CONF_ACCOUNT, CONF_ACCOUNT,
) )
ALL_PLATFORM = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
Platform.CLIMATE,
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def load_device_config(hass, device_type, sn8):
os.makedirs(hass.config.path(CONFIG_PATH), exist_ok=True)
config_file = hass.config.path(f"{CONFIG_PATH}/{sn8}.json")
json_data = load_json(config_file, default={})
d_type = "0x%02X" % device_type
if len(json_data) >0:
json_data = json_data.get(sn8)
elif d_type in DEVICE_MAPPING:
if sn8 in DEVICE_MAPPING[d_type]:
json_data = DEVICE_MAPPING[d_type][sn8]
save_data = {sn8: json_data}
save_json(config_file, save_data)
elif "default" in DEVICE_MAPPING[d_type]:
json_data = DEVICE_MAPPING[d_type]["default"]
save_data = {sn8: json_data}
save_json(config_file, save_data)
return json_data
async def update_listener(hass, config_entry):
pass
async def async_setup(hass: HomeAssistant, hass_config: dict): async def async_setup(hass: HomeAssistant, hass_config: dict):
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
cjson = os.getcwd() + "/cjson.lua" cjson = os.getcwd() + "/cjson.lua"
@@ -56,10 +95,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry):
port = config_entry.data.get(CONF_PORT) port = config_entry.data.get(CONF_PORT)
model = config_entry.data.get(CONF_MODEL) model = config_entry.data.get(CONF_MODEL)
protocol = config_entry.data.get(CONF_PROTOCOL) protocol = config_entry.data.get(CONF_PROTOCOL)
subtype = config_entry.data.get("subtype")
sn = config_entry.data.get("sn") sn = config_entry.data.get("sn")
sn8 = config_entry.data.get("sn8") sn8 = config_entry.data.get("sn8")
lua_file = config_entry.data.get("lua_file") lua_file = config_entry.data.get("lua_file")
_LOGGER.error(f"lua_file = {lua_file}")
if protocol == 3 and (key is None or key is None): if protocol == 3 and (key is None or key is None):
_LOGGER.error("For V3 devices, the key and the token is required.") _LOGGER.error("For V3 devices, the key and the token is required.")
return False return False
@@ -73,6 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry):
key=key, key=key,
protocol=protocol, protocol=protocol,
model=model, model=model,
subtype=subtype,
sn=sn, sn=sn,
sn8=sn8, sn8=sn8,
lua_file=lua_file, lua_file=lua_file,
@@ -83,10 +123,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry):
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
if DEVICES not in hass.data[DOMAIN]: if DEVICES not in hass.data[DOMAIN]:
hass.data[DOMAIN][DEVICES] = {} hass.data[DOMAIN][DEVICES] = {}
hass.data[DOMAIN][DEVICES][device_id] = device hass.data[DOMAIN][DEVICES][device_id] = {}
for platform in [Platform.BINARY_SENSOR]: hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device
hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {}
config = load_device_config(hass, device_type, sn8)
if config is not None and len(config) > 0:
queries = config.get("queries")
if queries is not None and isinstance(queries, list):
device.queries = queries
centralized = config.get("centralized")
if centralized is not None and isinstance(centralized, list):
device.centralized = centralized
hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer")
hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = config.get(CONF_ENTITIES)
for platform in ALL_PLATFORM:
hass.async_create_task(hass.config_entries.async_forward_entry_setup( hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, platform)) config_entry, platform))
#config_entry.add_update_listener(update_listener) config_entry.add_update_listener(update_listener)
return True return True
return False return False
async def async_unload_entry(hass: HomeAssistant, config_entry):
device_id = config_entry.data.get(CONF_DEVICE_ID)
lua_file = config_entry.data.get("lua_file")
os.remove(lua_file)
if device_id is not None:
device = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
if device is not None:
config_file = hass.config.path(f"{CONFIG_PATH}/{device.sn8}.json")
os.remove(config_file)
device.close()
hass.data[DOMAIN][DEVICES].pop(device_id)
for platform in ALL_PLATFORM:
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
return True

View File

@@ -1,48 +1,45 @@
import logging from homeassistant.components.binary_sensor import (
from homeassistant.const import ( BinarySensorEntity,
CONF_DEVICE_ID, BinarySensorDeviceClass
STATE_ON, )
STATE_OFF from homeassistant.const import (
Platform,
CONF_DEVICE_ID,
CONF_DEVICE,
CONF_ENTITIES
) )
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from .midea_entities import MideaEntity
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEVICES, DEVICES
) )
from .midea_entities import MideaBinaryBaseEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
device_id = config_entry.data.get(CONF_DEVICE_ID) device_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES].get(device_id) device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
binary_sensors = [] manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
sensor = MideaDeviceStatusSensor(device, "status") entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.BINARY_SENSOR)
binary_sensors.append(sensor) devs = [MideaDeviceStatusSensorEntity(device, manufacturer,"Status", {})]
async_add_entities(binary_sensors) if entities is not None:
for entity_key, config in entities.items():
devs.append(MideaBinarySensorEntity(device, manufacturer, entity_key, config))
async_add_entities(devs)
class MideaDeviceStatusSensor(MideaEntity): class MideaDeviceStatusSensorEntity(MideaBinaryBaseEntity, BinarySensorEntity):
@property @property
def device_class(self): def device_class(self):
return BinarySensorDeviceClass.CONNECTIVITY return BinarySensorDeviceClass.CONNECTIVITY
@property
def state(self):
return STATE_ON if self._device.connected else STATE_OFF
@property
def name(self):
return f"{self._device_name} Status"
@property @property
def icon(self): def icon(self):
return "mdi:devices" return "mdi:devices"
@property @property
def is_on(self): def is_on(self):
return self.state == STATE_ON return self._device.connected
@property @property
def available(self): def available(self):
@@ -57,3 +54,7 @@ class MideaDeviceStatusSensor(MideaEntity):
self.schedule_update_ha_state() self.schedule_update_ha_state()
except Exception as e: except Exception as e:
pass pass
class MideaBinarySensorEntity(MideaBinaryBaseEntity, BinarySensorEntity):
pass

View File

@@ -0,0 +1,191 @@
from homeassistant.components.climate import *
from homeassistant.const import (
Platform,
CONF_DEVICE_ID,
CONF_ENTITIES,
CONF_DEVICE,
)
from .const import (
DOMAIN,
DEVICES
)
from .core.logger import MideaLogger
from .midea_entities import MideaEntity, Rationale
async def async_setup_entry(hass, config_entry, async_add_entities):
device_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.CLIMATE)
devs = []
if entities is not None:
for entity_key, config in entities.items():
devs.append(MideaClimateEntity(device, manufacturer, entity_key, config))
async_add_entities(devs)
class MideaClimateEntity(MideaEntity, ClimateEntity):
def __init__(self, device, manufacturer, entity_key, config):
super().__init__(device, manufacturer, entity_key, config)
self._key_power = self._config.get("power")
self._key_hvac_modes = self._config.get("hvac_modes")
self._key_preset_modes = self._config.get("preset_modes")
self._key_aux_heat = self._config.get("aux_heat")
self._key_swing_modes = self._config.get("swing_modes")
self._key_fan_modes = self._config.get("fan_modes")
self._key_current_temperature_low = self._config.get("current_temperature_low")
self._key_min_temp = self._config.get("min_temp")
self._key_max_temp = self._config.get("max_temp")
self._key_target_temperature = self._config.get("target_temperature")
self._attr_temperature_unit = self._config.get("temperature_unit")
self._attr_precision = self._config.get("precision")
@property
def state(self):
return self.hvac_mode
@property
def supported_features(self):
features = 0
if self._key_target_temperature is not None:
features |= ClimateEntityFeature.TARGET_TEMPERATURE
if self._key_preset_modes is not None:
features |= ClimateEntityFeature.PRESET_MODE
if self._key_aux_heat is not None:
features |= ClimateEntityFeature.AUX_HEAT
if self._key_swing_modes is not None:
features |= ClimateEntityFeature.SWING_MODE
if self._key_fan_modes is not None:
features |= ClimateEntityFeature.FAN_MODE
return features
@property
def current_temperature(self):
return self._device.get_attribute("indoor_temperature")
@property
def target_temperature(self):
if isinstance(self._key_target_temperature, list):
temp_int = self._device.get_attribute(self._key_target_temperature[0])
tem_dec = self._device.get_attribute(self._key_target_temperature[1])
if temp_int is not None and tem_dec is not None:
return temp_int + tem_dec
return None
else:
return self._device.get_attribute(self._key_target_temperature)
@property
def min_temp(self):
if isinstance(self._key_min_temp, str):
return float(self._device.get_attribute(self._key_min_temp))
else:
return float(self._key_min_temp)
@property
def max_temp(self):
if isinstance(self._key_max_temp, str):
return float(self._device.get_attribute(self._key_max_temp))
else:
return float(self._key_max_temp)
@property
def target_temperature_low(self):
return self.min_temp
@property
def target_temperature_high(self):
return self.max_temp
@property
def preset_modes(self):
return list(self._key_preset_modes.keys())
@property
def preset_mode(self):
return self.get_mode(self._key_preset_modes)
@property
def fan_modes(self):
return list(self._key_fan_modes.keys())
@property
def fan_mode(self):
return self.get_mode(self._key_fan_modes, Rationale.LESS)
@property
def swing_modes(self):
return list(self._key_swing_modes.keys())
@property
def swing_mode(self):
return self.get_mode(self._key_swing_modes)
@property
def is_on(self) -> bool:
return self.hvac_mode != HVACMode.OFF
@property
def hvac_mode(self):
return self.get_mode(self._key_hvac_modes)
@property
def hvac_modes(self):
return list(self._key_hvac_modes.keys())
@property
def is_aux_heat(self):
return self._device.get_attribute(self._key_aux_heat) == "on"
def turn_on(self):
self._device.set_attribute(attribute=self._key_power, value="on")
def turn_off(self):
self._device.set_attribute(attribute=self._key_power, value="off")
def set_temperature(self, **kwargs):
if ATTR_TEMPERATURE not in kwargs:
return
temperature = kwargs.get(ATTR_TEMPERATURE)
temp_int, temp_dec = divmod(temperature, 1)
temp_int = int(temp_int)
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode is not None:
new_status = self._key_hvac_modes.get(hvac_mode)
else:
new_status = {}
if isinstance(self._key_target_temperature, list):
new_status[self._key_target_temperature[0]] = temp_int
new_status[self._key_target_temperature[1]] = temp_dec
else:
new_status[self._key_target_temperature] = temperature
MideaLogger.error(new_status)
self._device.set_attributes(new_status)
def set_fan_mode(self, fan_mode: str):
new_statis = self._key_fan_modes.get(fan_mode)
self._device.set_attributes(new_statis)
def set_preset_mode(self, preset_mode: str):
new_statis = self._key_preset_modes.get(preset_mode)
self._device.set_attributes(new_statis)
def set_hvac_mode(self, hvac_mode: str):
new_status = self._key_hvac_modes.get(hvac_mode)
self._device.set_attributes(new_status)
def set_swing_mode(self, swing_mode: str):
new_status = self._key_swing_modes.get(swing_mode)
self._device.set_attributes(new_status)
def turn_aux_heat_on(self) -> None:
self._device.set_attribute(attr=self._key_aux_heat, value="on")
def turn_aux_heat_off(self) -> None:
self._device.set_attribute(attr=self._key_aux_heat, value="off")
def update_state(self, status):
try:
self.schedule_update_ha_state()
except Exception as e:
pass

View File

@@ -129,6 +129,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"sn8": device.get("sn8"), "sn8": device.get("sn8"),
"sn": device.get("sn"), "sn": device.get("sn"),
"model": device.get("productModel"), "model": device.get("productModel"),
"subtype": int(device.get("modelNumber")) if device.get("modelNumber") is not None else 0,
"enterprise_code": device.get("enterpriseCode"), "enterprise_code": device.get("enterpriseCode"),
"online": device.get("onlineStatus") == "1" "online": device.get("onlineStatus") == "1"
} }
@@ -181,6 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
key=key, key=key,
protocol=3, protocol=3,
model=None, model=None,
subtype = None,
sn=None, sn=None,
sn8=None, sn8=None,
lua_file=None lua_file=None
@@ -202,6 +204,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
key=None, key=None,
protocol=2, protocol=2,
model=None, model=None,
subtype=None,
sn=None, sn=None,
sn8=None, sn8=None,
lua_file=None lua_file=None
@@ -220,19 +223,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_PROTOCOL: current_device.get("protocol"), CONF_PROTOCOL: current_device.get("protocol"),
CONF_IP_ADDRESS: current_device.get("ip_address"), CONF_IP_ADDRESS: current_device.get("ip_address"),
CONF_PORT: current_device.get("port"), CONF_PORT: current_device.get("port"),
CONF_MODEL: self._device.get("model"),
CONF_TOKEN: use_token, CONF_TOKEN: use_token,
CONF_KEY: use_key, CONF_KEY: use_key,
"lua_file": file, CONF_MODEL: self._device.get("model"),
"subtype": self._device.get("subtype"),
"sn": self._device.get("sn"), "sn": self._device.get("sn"),
"sn8": self._device.get("sn8"), "sn8": self._device.get("sn8"),
"lua_file": file,
}) })
else: else:
return await self.async_step_discover(error="invalid_input") return await self.async_step_discover(error="invalid_input")
return self.async_show_form( return self.async_show_form(
step_id="discover", step_id="discover",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_IP_ADDRESS): str vol.Required(CONF_IP_ADDRESS, default="auto"): str
}), }),
errors={"base": error} if error else None errors={"base": error} if error else None
) )

View File

@@ -1,5 +1,6 @@
DOMAIN = "midea_meiju_codec" DOMAIN = "midea_meiju_codec"
STORAGE_PATH = f".storage/{DOMAIN}" STORAGE_PATH = f".storage/{DOMAIN}/lua"
CONFIG_PATH = f".storage/{DOMAIN}/config"
DEVICES = "DEVICES" DEVICES = "DEVICES"
CONF_ACCOUNT = "account" CONF_ACCOUNT = "account"
CONF_HOME = "home" CONF_HOME = "home"

View File

@@ -1,18 +1,18 @@
from aiohttp import ClientSession
from secrets import token_hex, token_urlsafe
from .security import CloudSecurity
from threading import Lock
import datetime import datetime
import logging
import time import time
import json import json
import logging
_LOGGER = logging.getLogger(__name__) from aiohttp import ClientSession
from secrets import token_hex, token_urlsafe
from threading import Lock
from .security import CloudSecurity
CLIENT_TYPE = 1 # Android CLIENT_TYPE = 1 # Android
FORMAT = 2 # JSON FORMAT = 2 # JSON
APP_KEY = "4675636b" APP_KEY = "4675636b"
_LOGGER = logging.getLogger(__name__)
class MideaCloudBase: class MideaCloudBase:
LANGUAGE = "en_US" LANGUAGE = "en_US"
@@ -76,7 +76,7 @@ class MideaCloudBase:
response = json.loads(raw) response = json.loads(raw)
break break
except Exception as e: except Exception as e:
_LOGGER.debug(f"Cloud error: {repr(e)}") _LOGGER.error(f"Cloud error: {repr(e)}")
if int(response["code"]) == 0 and "data" in response: if int(response["code"]) == 0 and "data" in response:
return response["data"] return response["data"]
return None return None
@@ -127,7 +127,7 @@ class MideaCloudBase:
udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "big")) udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "big"))
else: else:
udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "little")) udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "little"))
_LOGGER.debug(f"The udpid of deivce [{device_id}] generated " _LOGGER.error(f"The udpid of deivce [{device_id}] generated "
f"with byte order '{'big' if byte_order_big else 'little'}': {udpid}") f"with byte order '{'big' if byte_order_big else 'little'}': {udpid}")
response = await self.api_request( response = await self.api_request(
"/v1/iot/secure/getToken", "/v1/iot/secure/getToken",

View File

@@ -1,14 +1,11 @@
import threading import threading
import socket
import time
from enum import IntEnum from enum import IntEnum
from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST from .security import LocalSecurity, MSGTYPE_HANDSHAKE_REQUEST, MSGTYPE_ENCRYPTED_REQUEST
from .packet_builder import PacketBuilder from .packet_builder import PacketBuilder
from .lua_runtime import MideaCodec from .lua_runtime import MideaCodec
import socket from .logger import MideaLogger
import logging
import json
import time
_LOGGER = logging.getLogger(__name__)
class AuthException(Exception): class AuthException(Exception):
@@ -40,6 +37,7 @@ class MiedaDevice(threading.Thread):
key: str | None, key: str | None,
protocol: int, protocol: int,
model: str | None, model: str | None,
subtype: int | None,
sn: str | None, sn: str | None,
sn8: str | None, sn8: str | None,
lua_file: str | None): lua_file: str | None):
@@ -57,17 +55,21 @@ class MiedaDevice(threading.Thread):
self._protocol = protocol self._protocol = protocol
self._model = model self._model = model
self._updates = [] self._updates = []
self._unsupported_protocol = []
self._is_run = False self._is_run = False
self._device_protocol_version = 0 self._subtype = subtype
self._sub_type = None self._sn = sn
self._sn8 = sn8 self._sn8 = sn8
self._attributes = {} self._attributes = {
"sn": sn,
"sn8": sn8,
"subtype": subtype
}
self._refresh_interval = 30 self._refresh_interval = 30
self._heartbeat_interval = 10 self._heartbeat_interval = 10
self._default_refresh_interval = 30
self._connected = False self._connected = False
self._lua_runtime = MideaCodec(lua_file, sn=sn) if lua_file is not None else None self._queries = [{}]
self._centralized = []
self._lua_runtime = MideaCodec(lua_file, sn=sn, subtype=subtype) if lua_file is not None else None
@property @property
def device_name(self): def device_name(self):
@@ -93,6 +95,10 @@ class MiedaDevice(threading.Thread):
def sn8(self): def sn8(self):
return self._sn8 return self._sn8
@property
def subtype(self):
return self._subtype
@property @property
def attributes(self): def attributes(self):
return self._attributes return self._attributes
@@ -101,6 +107,50 @@ class MiedaDevice(threading.Thread):
def connected(self): def connected(self):
return self._connected return self._connected
def set_refresh_interval(self, refresh_interval):
self._refresh_interval = refresh_interval
@property
def queries(self):
return self._queries
@queries.setter
def queries(self, queries: list):
self._queries = queries
@property
def centralized(self):
return self._centralized
@centralized.setter
def centralized(self, centralized: list):
self._centralized = centralized
def get_attribute(self, attribute):
return self._attributes.get(attribute)
def set_attribute(self, attribute, value):
if attribute in self._attributes.keys():
new_status = {}
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
new_status[attribute] = value
set_cmd = self._lua_runtime.build_control(new_status)
self.build_send(set_cmd)
def set_attributes(self, attributes):
new_status = {}
for attr in self._centralized:
new_status[attr] = self._attributes.get(attr)
has_new = False
for attribute, value in attributes.items():
if attribute in self._attributes.keys():
has_new = True
new_status[attribute] = value
if has_new:
set_cmd = self._lua_runtime.build_control(new_status)
self.build_send(set_cmd)
@staticmethod @staticmethod
def fetch_v2_message(msg): def fetch_v2_message(msg):
result = [] result = []
@@ -120,28 +170,28 @@ class MiedaDevice(threading.Thread):
try: try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10) self._socket.settimeout(10)
_LOGGER.debug(f"[{self._device_id}] Connecting to {self._ip_address}:{self._port}") MideaLogger.debug(f"Connecting to {self._ip_address}:{self._port}", self._device_id)
self._socket.connect((self._ip_address, self._port)) self._socket.connect((self._ip_address, self._port))
_LOGGER.debug(f"[{self._device_id}] Connected") MideaLogger.debug(f"Connected", self._device_id)
if self._protocol == 3: if self._protocol == 3:
self.authenticate() self.authenticate()
_LOGGER.debug(f"[{self._device_id}] Authentication success") MideaLogger.debug(f"Authentication success", self._device_id)
self.device_connected(True) self.device_connected(True)
if refresh: if refresh:
self.refresh_status() self.refresh_status()
return True return True
except socket.timeout: except socket.timeout:
_LOGGER.debug(f"[{self._device_id}] Connection timed out") MideaLogger.debug(f"Connection timed out", self._device_id)
except socket.error: except socket.error:
_LOGGER.debug(f"[{self._device_id}] Connection error") MideaLogger.debug(f"Connection error", self._device_id)
except AuthException: except AuthException:
_LOGGER.debug(f"[{self._device_id}] Authentication failed") MideaLogger.debug(f"Authentication failed", self._device_id)
except ResponseException: except ResponseException:
_LOGGER.debug(f"[{self._device_id}] Unexpected response received") MideaLogger.debug(f"Unexpected response received", self._device_id)
except RefreshFailed: except RefreshFailed:
_LOGGER.debug(f"[{self._device_id}] Refresh status is timed out") MideaLogger.debug(f"Refresh status is timed out", self._device_id)
except Exception as e: except Exception as e:
_LOGGER.error(f"[{self._device_id}] Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, " MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}") f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.device_connected(False) self.device_connected(False)
return False return False
@@ -149,7 +199,7 @@ class MiedaDevice(threading.Thread):
def authenticate(self): def authenticate(self):
request = self._security.encode_8370( request = self._security.encode_8370(
self._token, MSGTYPE_HANDSHAKE_REQUEST) self._token, MSGTYPE_HANDSHAKE_REQUEST)
_LOGGER.debug(f"[{self._device_id}] Handshaking") MideaLogger.debug(f"Handshaking")
self._socket.send(request) self._socket.send(request)
response = self._socket.recv(512) response = self._socket.recv(512)
if len(response) < 20: if len(response) < 20:
@@ -167,20 +217,21 @@ class MiedaDevice(threading.Thread):
if self._socket is not None: if self._socket is not None:
self._socket.send(data) self._socket.send(data)
else: else:
_LOGGER.debug(f"[{self._device_id}] Send failure, device disconnected, data: {data.hex()}") MideaLogger.debug(f"Send failure, device disconnected, data: {data.hex()}")
def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST): def send_message_v3(self, data, msg_type=MSGTYPE_ENCRYPTED_REQUEST):
data = self._security.encode_8370(data, msg_type) data = self._security.encode_8370(data, msg_type)
self.send_message_v2(data) self.send_message_v2(data)
def build_send(self, cmd): def build_send(self, cmd):
_LOGGER.debug(f"[{self._device_id}] Sending: {cmd}") MideaLogger.debug(f"Sending: {cmd}")
bytes_cmd = bytes.fromhex(cmd) bytes_cmd = bytes.fromhex(cmd)
msg = PacketBuilder(self._device_id, bytes_cmd).finalize() msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
self.send_message(msg) self.send_message(msg)
def refresh_status(self): def refresh_status(self):
query_cmd = self._lua_runtime.build_query() for query in self._queries:
query_cmd = self._lua_runtime.build_query(query)
self.build_send(query_cmd) self.build_send(query_cmd)
def parse_message(self, msg): def parse_message(self, msg):
@@ -202,10 +253,10 @@ class MiedaDevice(threading.Thread):
cryptographic = message[40:-16] cryptographic = message[40:-16]
if payload_len % 16 == 0: if payload_len % 16 == 0:
decrypted = self._security.aes_decrypt(cryptographic) decrypted = self._security.aes_decrypt(cryptographic)
_LOGGER.debug(f"[{self._device_id}] Received: {decrypted.hex()}") MideaLogger.debug(f"Received: {decrypted.hex()}")
# 这就是最终消息 # 这就是最终消息
status = self._lua_runtime.decode_status(decrypted.hex()) status = self._lua_runtime.decode_status(decrypted.hex())
_LOGGER.debug(f"[{self._device_id}] Decoded: {status}") MideaLogger.debug(f"Decoded: {status}")
new_status = {} new_status = {}
for single in status.keys(): for single in status.keys():
value = status.get(single) value = status.get(single)
@@ -229,7 +280,7 @@ class MiedaDevice(threading.Thread):
self._updates.append(update) self._updates.append(update)
def update_all(self, status): def update_all(self, status):
_LOGGER.debug(f"[{self._device_id}] Status update: {status}") MideaLogger.debug(f"Status update: {status}")
for update in self._updates: for update in self._updates:
update(status) update(status)
@@ -241,17 +292,17 @@ class MiedaDevice(threading.Thread):
def close(self): def close(self):
if self._is_run: if self._is_run:
self._is_run = False self._is_run = False
self._lua_runtime = None
self.close_socket() self.close_socket()
def close_socket(self): def close_socket(self):
self._unsupported_protocol = []
self._buffer = b"" self._buffer = b""
if self._socket: if self._socket:
self._socket.close() self._socket.close()
self._socket = None self._socket = None
def set_ip_address(self, ip_address): def set_ip_address(self, ip_address):
_LOGGER.debug(f"[{self._device_id}] Update IP address to {ip_address}") MideaLogger.debug(f"Update IP address to {ip_address}")
self._ip_address = ip_address self._ip_address = ip_address
self.close_socket() self.close_socket()
@@ -283,7 +334,7 @@ class MiedaDevice(threading.Thread):
raise socket.error("Connection closed by peer") raise socket.error("Connection closed by peer")
result = self.parse_message(msg) result = self.parse_message(msg)
if result == ParseMessageResult.ERROR: if result == ParseMessageResult.ERROR:
_LOGGER.debug(f"[{self._device_id}] Message 'ERROR' received") MideaLogger.debug(f"Message 'ERROR' received")
self.close_socket() self.close_socket()
break break
elif result == ParseMessageResult.SUCCESS: elif result == ParseMessageResult.SUCCESS:
@@ -291,15 +342,15 @@ class MiedaDevice(threading.Thread):
except socket.timeout: except socket.timeout:
timeout_counter = timeout_counter + 1 timeout_counter = timeout_counter + 1
if timeout_counter >= 120: if timeout_counter >= 120:
_LOGGER.debug(f"[{self._device_id}] Heartbeat timed out") MideaLogger.debug(f"Heartbeat timed out")
self.close_socket() self.close_socket()
break break
except socket.error as e: except socket.error as e:
_LOGGER.debug(f"[{self._device_id}] Socket error {repr(e)}") MideaLogger.debug(f"Socket error {repr(e)}")
self.close_socket() self.close_socket()
break break
except Exception as e: except Exception as e:
_LOGGER.error(f"[{self._device_id}] Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, " MideaLogger.error(f"Unknown error :{e.__traceback__.tb_frame.f_globals['__file__']}, "
f"{e.__traceback__.tb_lineno}, {repr(e)}") f"{e.__traceback__.tb_lineno}, {repr(e)}")
self.close_socket() self.close_socket()
break break

View File

@@ -1,14 +1,13 @@
import logging
import socket import socket
import ifaddr import ifaddr
from ipaddress import IPv4Network from ipaddress import IPv4Network
from .security import LocalSecurity from .security import LocalSecurity
from .logger import MideaLogger
try: try:
import xml.etree.cElementTree as ET import xml.etree.cElementTree as ET
except ImportError: except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
_LOGGER = logging.getLogger(__name__)
BROADCAST_MSG = bytearray([ BROADCAST_MSG = bytearray([
0x5a, 0x5a, 0x01, 0x11, 0x48, 0x00, 0x92, 0x00, 0x5a, 0x5a, 0x01, 0x11, 0x48, 0x00, 0x92, 0x00,
@@ -34,7 +33,7 @@ DEVICE_INFO_MSG = bytearray([
def discover(discover_type=None, ip_address=None): def discover(discover_type=None, ip_address=None):
_LOGGER.debug(f"Begin discover, type: {discover_type}, ip_address: {ip_address}") MideaLogger.debug(f"Begin discover, type: {discover_type}, ip_address: {ip_address}")
if discover_type is None: if discover_type is None:
discover_type = [] discover_type = []
security = LocalSecurity() security = LocalSecurity()
@@ -55,7 +54,7 @@ def discover(discover_type=None, ip_address=None):
try: try:
data, addr = sock.recvfrom(512) data, addr = sock.recvfrom(512)
ip = addr[0] ip = addr[0]
_LOGGER.debug(f"Received broadcast from {addr}: {data.hex()}") MideaLogger.debug(f"Received broadcast from {addr}: {data.hex()}")
if len(data) >= 104 and (data[:2].hex() == "5a5a" or data[8:10].hex() == "5a5a"): if len(data) >= 104 and (data[:2].hex() == "5a5a" or data[8:10].hex() == "5a5a"):
if data[:2].hex() == "5a5a": if data[:2].hex() == "5a5a":
protocol = 2 protocol = 2
@@ -70,7 +69,7 @@ def discover(discover_type=None, ip_address=None):
continue continue
encrypt_data = data[40:-16] encrypt_data = data[40:-16]
reply = security.aes_decrypt(encrypt_data) reply = security.aes_decrypt(encrypt_data)
_LOGGER.debug(f"Declassified reply: {reply.hex()}") MideaLogger.debug(f"Declassified reply: {reply.hex()}")
ssid = reply[41:41 + reply[40]].decode("utf-8") ssid = reply[41:41 + reply[40]].decode("utf-8")
device_type = ssid.split("_")[1] device_type = ssid.split("_")[1]
port = bytes2port(reply[4:8]) port = bytes2port(reply[4:8])
@@ -105,13 +104,13 @@ def discover(discover_type=None, ip_address=None):
} }
if len(discover_type) == 0 or device.get("type") in discover_type: if len(discover_type) == 0 or device.get("type") in discover_type:
found_devices[device_id] = device found_devices[device_id] = device
_LOGGER.debug(f"Found a supported device: {device}") MideaLogger.debug(f"Found a supported device: {device}")
else: else:
_LOGGER.debug(f"Found a unsupported device: {device}") MideaLogger.debug(f"Found a unsupported device: {device}")
except socket.timeout: except socket.timeout:
break break
except socket.error as e: except socket.error as e:
_LOGGER.debug(f"Socket error: {repr(e)}") MideaLogger.debug(f"Socket error: {repr(e)}")
return found_devices return found_devices
@@ -147,15 +146,15 @@ def get_device_info(device_ip, device_port: int):
sock.settimeout(8) sock.settimeout(8)
device_address = (device_ip, device_port) device_address = (device_ip, device_port)
sock.connect(device_address) sock.connect(device_address)
_LOGGER.debug(f"Sending to {device_ip}:{device_port} {DEVICE_INFO_MSG.hex()}") MideaLogger.debug(f"Sending to {device_ip}:{device_port} {DEVICE_INFO_MSG.hex()}")
sock.sendall(DEVICE_INFO_MSG) sock.sendall(DEVICE_INFO_MSG)
response = sock.recv(512) response = sock.recv(512)
except socket.timeout: except socket.timeout:
_LOGGER.warning(f"Connect the device {device_ip}:{device_port} timed out for 8s. " MideaLogger.warning(f"Connect the device {device_ip}:{device_port} timed out for 8s. "
f"Don't care about a small amount of this. if many maybe not support." f"Don't care about a small amount of this. if many maybe not support."
) )
except socket.error: except socket.error:
_LOGGER.warning(f"Can't connect to Device {device_ip}:{device_port}") MideaLogger.warning(f"Can't connect to Device {device_ip}:{device_port}")
return response return response

View File

@@ -0,0 +1,36 @@
import inspect
import logging
from enum import IntEnum
class MideaLogType(IntEnum):
DEBUG = 1
WARN = 2
ERROR = 3
class MideaLogger:
@staticmethod
def _log(log_type, log, device_id):
frm = inspect.stack()[2]
mod = inspect.getmodule(frm[0])
if device_id is not None:
log = f"[{device_id}] {log}"
if log_type == MideaLogType.DEBUG:
logging.getLogger(mod.__name__).debug(log)
elif log_type == MideaLogType.WARN:
logging.getLogger(mod.__name__).warning(log)
elif log_type == MideaLogType.ERROR:
logging.getLogger(mod.__name__).error(log)
@staticmethod
def debug(log, device_id=None):
MideaLogger._log(MideaLogType.DEBUG, log, device_id)
@staticmethod
def warning(log, device_id=None):
MideaLogger._log(MideaLogType.WARN, log, device_id)
@staticmethod
def error(log, device_id=None):
MideaLogger._log(MideaLogType.ERROR, log, device_id)

View File

@@ -1,10 +1,7 @@
import lupa import lupa
import logging
import threading import threading
import json import json
_LOGGER = logging.getLogger(__name__)
class LuaRuntime: class LuaRuntime:
def __init__(self, file): def __init__(self, file):
@@ -27,17 +24,17 @@ class LuaRuntime:
class MideaCodec(LuaRuntime): class MideaCodec(LuaRuntime):
def __init__(self, file, sn=None, sub_type=None): def __init__(self, file, sn=None, subtype=None):
super().__init__(file) super().__init__(file)
self._sn = sn self._sn = sn
self._sub_type = sub_type self._subtype = subtype
def _build_base_dict(self): def _build_base_dict(self):
device_info ={} device_info ={}
if self._sn is not None: if self._sn is not None:
device_info["deviceSN"] = self._sn device_info["deviceSN"] = self._sn
if self._sub_type is not None: if self._subtype is not None:
device_info["deviceSubType"] = self._sub_type device_info["deviceSubType"] = self._subtype
base_dict = { base_dict = {
"deviceinfo": device_info "deviceinfo": device_info
} }

View File

@@ -6,7 +6,6 @@ class PacketBuilder:
def __init__(self, device_id: int, command): def __init__(self, device_id: int, command):
self.command = None self.command = None
self.security = LocalSecurity() self.security = LocalSecurity()
# aa20ac00000000000003418100ff03ff000200000000000000000000000006f274
# Init the packet with the header data. # Init the packet with the header data.
self.packet = bytearray([ self.packet = bytearray([
# 2 bytes - StaicHeader # 2 bytes - StaicHeader

View File

@@ -1,20 +1,18 @@
import hmac
import logging import logging
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad from Crypto.Util.Padding import pad, unpad
from Crypto.Util.strxor import strxor from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
from hashlib import md5, sha256 from hashlib import md5, sha256
from urllib.parse import urlparse
import hmac
import urllib
_LOGGER = logging.getLogger(__name__)
MSGTYPE_HANDSHAKE_REQUEST = 0x0 MSGTYPE_HANDSHAKE_REQUEST = 0x0
MSGTYPE_HANDSHAKE_RESPONSE = 0x1 MSGTYPE_HANDSHAKE_RESPONSE = 0x1
MSGTYPE_ENCRYPTED_RESPONSE = 0x3 MSGTYPE_ENCRYPTED_RESPONSE = 0x3
MSGTYPE_ENCRYPTED_REQUEST = 0x6 MSGTYPE_ENCRYPTED_REQUEST = 0x6
_LOGGER = logging.getLogger(__name__)
class CloudSecurity: class CloudSecurity:

View File

@@ -0,0 +1,106 @@
from homeassistant.const import *
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.components.climate import (
HVACMode,
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_SLEEP,
PRESET_BOOST,
SWING_OFF,
SWING_BOTH,
SWING_VERTICAL,
SWING_HORIZONTAL,
FAN_AUTO,
FAN_LOW,
FAN_MEDIUM,
FAN_HIGH,
)
DEVICE_MAPPING = {
"0xAC": {
"default": {
"manufacturer": "美的",
"queries": [{}, {"query_type": "prevent_straight_wind"}],
"centralized": ["power", "temperature", "small_temperature", "mode", "eco", "comfort_power_save",
"comfort_sleep", "strong_wind", "wind_swing_lr", "wind_swing_lr", "wind_speed",
"ptc", "dry"],
"entities": {
Platform.CLIMATE: {
"thermostat": {
"name": "Thermostat",
"power": "power",
"target_temperature": ["temperature", "small_temperature"],
"hvac_modes": {
HVACMode.OFF: {"power": "off"},
HVACMode.HEAT: {"power": "on", "mode": "heat"},
HVACMode.COOL: {"power": "on", "mode": "cool"},
HVACMode.AUTO: {"power": "on", "mode": "auto"},
HVACMode.DRY: {"power": "on", "mode": "dry"},
HVACMode.FAN_ONLY: {"power": "on", "mode": "fan"}
},
"preset_modes": {
PRESET_NONE: {
"eco": "off",
"comfort_power_save": "off",
"comfort_sleep": "off",
"strong_wind": "off"
},
PRESET_ECO: {"eco": "on"},
PRESET_COMFORT: {"comfort_power_save": "on"},
PRESET_SLEEP: {"comfort_sleep": "on"},
PRESET_BOOST: {"strong_wind": "on"}
},
"swing_modes": {
SWING_OFF: {"wind_swing_lr": "off", "wind_swing_ud": "off"},
SWING_BOTH: {"wind_swing_lr": "on", "wind_swing_ud": "on"},
SWING_HORIZONTAL: {"wind_swing_lr": "on", "wind_swing_ud": "off"},
SWING_VERTICAL: {"wind_swing_lr": "off", "wind_swing_ud": "on"},
},
"fan_modes": {
"silent": {"wind_speed": 20},
FAN_LOW: {"wind_speed": 40},
FAN_MEDIUM: {"wind_speed": 60},
FAN_HIGH: {"wind_speed": 80},
"full": {"wind_speed": 100},
FAN_AUTO: {"wind_speed": 102}
},
"current_temperature": "indoor_temperature",
"aux_heat": "ptc",
"min_temp": 17,
"max_temp": 30,
"temperature_unit": TEMP_CELSIUS,
"precision": PRECISION_HALVES,
}
},
Platform.SWITCH: {
"dry": {
"name": "Dry",
"device_class": SwitchDeviceClass.SWITCH,
},
"prevent_straight_wind": {
"binary_rationale": [1, 2]
}
},
Platform.SENSOR: {
"indoor_temperature": {
"name": "室内温度",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": TEMP_CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
"outdoor_temperature": {
"name": "室外机温度",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": TEMP_CELSIUS,
"state_class": SensorStateClass.MEASUREMENT
},
},
Platform.BINARY_SENSOR: {
"power": {}
}
}
}
},
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"issue_tracker": "https://github.com/georgezhao2010/midea-meiju-codec/issues", "issue_tracker": "https://github.com/georgezhao2010/midea-meiju-codec/issues",
"requirements": ["lupa>=2.0"], "requirements": ["lupa>=2.0"],
"version": "v0.0.2" "version": "v0.0.3"
} }

View File

@@ -1,32 +1,43 @@
from enum import IntEnum
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import (
STATE_ON,
STATE_OFF
)
from .const import DOMAIN from .const import DOMAIN
from .core.logger import MideaLogger
class Rationale(IntEnum):
EQUALLY = 0
GREATER = 1
LESS = 2
class MideaEntity(Entity): class MideaEntity(Entity):
def __init__(self, device, entity_key: str): def __init__(self, device, manufacturer: str | None, entity_key: str, config: dict):
self._device = device self._device = device
self._device.register_update(self.update_state) self._device.register_update(self.update_state)
self._entity_key = entity_key self._entity_key = entity_key
self._unique_id = f"{DOMAIN}.{self._device.device_id}_{entity_key}" self._config = config
self.entity_id = self._unique_id
self._device_name = self._device.device_name self._device_name = self._device.device_name
self._attr_native_unit_of_measurement = self._config.get("unit")
@property self._attr_device_class = self._config.get("device_class")
def device(self): self._attr_state_class = self._config.get("state_class")
return self._device self._attr_unit_of_measurement = self._config.get("unit")
self._attr_icon = self._config.get("icon")
@property self._attr_unique_id = f"{DOMAIN}.{self._device.device_id}_{self._entity_key}"
def device_info(self): MideaLogger.debug(self._attr_unique_id)
return { self._attr_device_info = {
"manufacturer": "Midea", "manufacturer": "Midea" if manufacturer is None else manufacturer,
"model": f"{self._device.model} ({self._device.sn8})", "model": f"{self._device.model}",
"identifiers": {(DOMAIN, self._device.device_id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"name": self._device_name "name": self._device_name
} }
name = self._config.get("name")
@property if name is None:
def unique_id(self): name = self._entity_key.replace("_", " ").title()
return self._unique_id self._attr_name = f"{self._device_name} {name}"
self.entity_id = self._attr_unique_id
@property @property
def should_poll(self): def should_poll(self):
@@ -34,15 +45,52 @@ class MideaEntity(Entity):
@property @property
def state(self): def state(self):
return self._device.get_attribute(self._entity_key) raise NotImplementedError
@property @property
def available(self): def available(self):
return self._device.connected return self._device.connected
def get_mode(self, key_of_modes, rationale: Rationale = Rationale.EQUALLY):
for mode, status in key_of_modes.items():
match = True
for attr, value in status.items():
state_value = self._device.get_attribute(attr)
if state_value is None:
match = False
break
if rationale is Rationale.EQUALLY and state_value != value:
match = False
break
if rationale is Rationale.GREATER and state_value < value:
match = False
break
if rationale is Rationale.LESS and state_value > value:
match = False
break
if match:
return mode
return None
def update_state(self, status): def update_state(self, status):
if self._entity_key in status or "connected" in status: if self._entity_key in status or "connected" in status:
try: try:
self.schedule_update_ha_state() self.schedule_update_ha_state()
except Exception as e: except Exception as e:
pass pass
class MideaBinaryBaseEntity(MideaEntity):
def __init__(self, device, manufacturer: str | None, entity_key: str, config: dict):
super().__init__(device, manufacturer, entity_key, config)
binary_rationale = config.get("binary_rationale")
self._binary_rationale = binary_rationale if binary_rationale is not None else ["off", "on"]
@property
def state(self):
return STATE_ON if self.is_on else STATE_OFF
@property
def is_on(self):
return self._device.get_attribute(self._entity_key) == self._binary_rationale[1]

View File

@@ -0,0 +1,31 @@
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
Platform,
CONF_DEVICE_ID,
CONF_DEVICE,
CONF_ENTITIES
)
from .const import (
DOMAIN,
DEVICES
)
from .midea_entities import MideaEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
device_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SENSOR)
devs = []
if entities is not None:
for entity_key, config in entities.items():
devs.append(MideaSensorEntity(device, manufacturer, entity_key, config))
async_add_entities(devs)
class MideaSensorEntity(MideaEntity, SensorEntity):
@property
def state(self):
return self._device.get_attribute(self._entity_key)

View File

@@ -0,0 +1,35 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import (
Platform,
CONF_DEVICE_ID,
CONF_DEVICE,
CONF_ENTITIES,
STATE_ON,
STATE_OFF
)
from .const import (
DOMAIN,
DEVICES
)
from .midea_entities import MideaBinaryBaseEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
device_id = config_entry.data.get(CONF_DEVICE_ID)
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SWITCH)
devs = []
if entities is not None:
for entity_key, config in entities.items():
devs.append(MideaSwitchEntity(device, manufacturer, entity_key, config))
async_add_entities(devs)
class MideaSwitchEntity(MideaBinaryBaseEntity, SwitchEntity):
def turn_on(self):
self._device.set_attribute(attribute=self._entity_key, value=self._binary_rationale[1])
def turn_off(self):
self._device.set_attribute(attribute=self._entity_key, value=self._binary_rationale[0])