mirror of
https://github.com/sususweet/midea-meiju-codec.git
synced 2025-09-27 18:22:41 +00:00
v0.0.3
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
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.const import (
|
||||
Platform,
|
||||
@@ -11,19 +16,53 @@ from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TOKEN,
|
||||
CONF_NAME
|
||||
CONF_NAME,
|
||||
CONF_DEVICE,
|
||||
CONF_ENTITIES
|
||||
)
|
||||
from .device_map.device_mapping import DEVICE_MAPPING
|
||||
from .core.device import MiedaDevice
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICES,
|
||||
CONFIG_PATH,
|
||||
CONF_KEY,
|
||||
CONF_ACCOUNT,
|
||||
)
|
||||
|
||||
ALL_PLATFORM = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.CLIMATE,
|
||||
]
|
||||
|
||||
_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):
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
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)
|
||||
model = config_entry.data.get(CONF_MODEL)
|
||||
protocol = config_entry.data.get(CONF_PROTOCOL)
|
||||
subtype = config_entry.data.get("subtype")
|
||||
sn = config_entry.data.get("sn")
|
||||
sn8 = config_entry.data.get("sn8")
|
||||
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):
|
||||
_LOGGER.error("For V3 devices, the key and the token is required.")
|
||||
return False
|
||||
@@ -73,6 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry):
|
||||
key=key,
|
||||
protocol=protocol,
|
||||
model=model,
|
||||
subtype=subtype,
|
||||
sn=sn,
|
||||
sn8=sn8,
|
||||
lua_file=lua_file,
|
||||
@@ -83,10 +123,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry):
|
||||
hass.data[DOMAIN] = {}
|
||||
if DEVICES not in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN][DEVICES] = {}
|
||||
hass.data[DOMAIN][DEVICES][device_id] = device
|
||||
for platform in [Platform.BINARY_SENSOR]:
|
||||
hass.data[DOMAIN][DEVICES][device_id] = {}
|
||||
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(
|
||||
config_entry, platform))
|
||||
#config_entry.add_update_listener(update_listener)
|
||||
config_entry.add_update_listener(update_listener)
|
||||
return True
|
||||
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
|
||||
|
@@ -1,48 +1,45 @@
|
||||
import logging
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
STATE_ON,
|
||||
STATE_OFF
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorDeviceClass
|
||||
)
|
||||
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 (
|
||||
DOMAIN,
|
||||
DEVICES,
|
||||
DEVICES
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
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].get(device_id)
|
||||
binary_sensors = []
|
||||
sensor = MideaDeviceStatusSensor(device, "status")
|
||||
binary_sensors.append(sensor)
|
||||
async_add_entities(binary_sensors)
|
||||
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.BINARY_SENSOR)
|
||||
devs = [MideaDeviceStatusSensorEntity(device, manufacturer,"Status", {})]
|
||||
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
|
||||
def device_class(self):
|
||||
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
|
||||
def icon(self):
|
||||
return "mdi:devices"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self.state == STATE_ON
|
||||
return self._device.connected
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -57,3 +54,7 @@ class MideaDeviceStatusSensor(MideaEntity):
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
class MideaBinarySensorEntity(MideaBinaryBaseEntity, BinarySensorEntity):
|
||||
pass
|
||||
|
191
custom_components/midea_meiju_codec/climate.py
Normal file
191
custom_components/midea_meiju_codec/climate.py
Normal 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
|
@@ -129,6 +129,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"sn8": device.get("sn8"),
|
||||
"sn": device.get("sn"),
|
||||
"model": device.get("productModel"),
|
||||
"subtype": int(device.get("modelNumber")) if device.get("modelNumber") is not None else 0,
|
||||
"enterprise_code": device.get("enterpriseCode"),
|
||||
"online": device.get("onlineStatus") == "1"
|
||||
}
|
||||
@@ -181,6 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
key=key,
|
||||
protocol=3,
|
||||
model=None,
|
||||
subtype = None,
|
||||
sn=None,
|
||||
sn8=None,
|
||||
lua_file=None
|
||||
@@ -202,6 +204,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
key=None,
|
||||
protocol=2,
|
||||
model=None,
|
||||
subtype=None,
|
||||
sn=None,
|
||||
sn8=None,
|
||||
lua_file=None
|
||||
@@ -220,19 +223,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: current_device.get("protocol"),
|
||||
CONF_IP_ADDRESS: current_device.get("ip_address"),
|
||||
CONF_PORT: current_device.get("port"),
|
||||
CONF_MODEL: self._device.get("model"),
|
||||
CONF_TOKEN: use_token,
|
||||
CONF_KEY: use_key,
|
||||
"lua_file": file,
|
||||
CONF_MODEL: self._device.get("model"),
|
||||
"subtype": self._device.get("subtype"),
|
||||
"sn": self._device.get("sn"),
|
||||
"sn8": self._device.get("sn8"),
|
||||
"lua_file": file,
|
||||
})
|
||||
else:
|
||||
return await self.async_step_discover(error="invalid_input")
|
||||
return self.async_show_form(
|
||||
step_id="discover",
|
||||
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
|
||||
)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
DOMAIN = "midea_meiju_codec"
|
||||
STORAGE_PATH = f".storage/{DOMAIN}"
|
||||
STORAGE_PATH = f".storage/{DOMAIN}/lua"
|
||||
CONFIG_PATH = f".storage/{DOMAIN}/config"
|
||||
DEVICES = "DEVICES"
|
||||
CONF_ACCOUNT = "account"
|
||||
CONF_HOME = "home"
|
||||
|
@@ -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 logging
|
||||
import time
|
||||
import json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
import logging
|
||||
from aiohttp import ClientSession
|
||||
from secrets import token_hex, token_urlsafe
|
||||
from threading import Lock
|
||||
from .security import CloudSecurity
|
||||
|
||||
CLIENT_TYPE = 1 # Android
|
||||
FORMAT = 2 # JSON
|
||||
APP_KEY = "4675636b"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MideaCloudBase:
|
||||
LANGUAGE = "en_US"
|
||||
@@ -76,7 +76,7 @@ class MideaCloudBase:
|
||||
response = json.loads(raw)
|
||||
break
|
||||
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:
|
||||
return response["data"]
|
||||
return None
|
||||
@@ -127,7 +127,7 @@ class MideaCloudBase:
|
||||
udpid = CloudSecurity.get_udpid(device_id.to_bytes(6, "big"))
|
||||
else:
|
||||
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}")
|
||||
response = await self.api_request(
|
||||
"/v1/iot/secure/getToken",
|
||||
|
@@ -1,14 +1,11 @@
|
||||
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
|
||||
import socket
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .logger import MideaLogger
|
||||
|
||||
|
||||
class AuthException(Exception):
|
||||
@@ -40,6 +37,7 @@ class MiedaDevice(threading.Thread):
|
||||
key: str | None,
|
||||
protocol: int,
|
||||
model: str | None,
|
||||
subtype: int | None,
|
||||
sn: str | None,
|
||||
sn8: str | None,
|
||||
lua_file: str | None):
|
||||
@@ -57,17 +55,21 @@ class MiedaDevice(threading.Thread):
|
||||
self._protocol = protocol
|
||||
self._model = model
|
||||
self._updates = []
|
||||
self._unsupported_protocol = []
|
||||
self._is_run = False
|
||||
self._device_protocol_version = 0
|
||||
self._sub_type = None
|
||||
self._subtype = subtype
|
||||
self._sn = sn
|
||||
self._sn8 = sn8
|
||||
self._attributes = {}
|
||||
self._attributes = {
|
||||
"sn": sn,
|
||||
"sn8": sn8,
|
||||
"subtype": subtype
|
||||
}
|
||||
self._refresh_interval = 30
|
||||
self._heartbeat_interval = 10
|
||||
self._default_refresh_interval = 30
|
||||
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
|
||||
def device_name(self):
|
||||
@@ -93,6 +95,10 @@ class MiedaDevice(threading.Thread):
|
||||
def sn8(self):
|
||||
return self._sn8
|
||||
|
||||
@property
|
||||
def subtype(self):
|
||||
return self._subtype
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self._attributes
|
||||
@@ -101,6 +107,50 @@ class MiedaDevice(threading.Thread):
|
||||
def connected(self):
|
||||
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
|
||||
def fetch_v2_message(msg):
|
||||
result = []
|
||||
@@ -120,28 +170,28 @@ class MiedaDevice(threading.Thread):
|
||||
try:
|
||||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
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))
|
||||
_LOGGER.debug(f"[{self._device_id}] Connected")
|
||||
MideaLogger.debug(f"Connected", self._device_id)
|
||||
if self._protocol == 3:
|
||||
self.authenticate()
|
||||
_LOGGER.debug(f"[{self._device_id}] Authentication success")
|
||||
MideaLogger.debug(f"Authentication success", self._device_id)
|
||||
self.device_connected(True)
|
||||
if refresh:
|
||||
self.refresh_status()
|
||||
return True
|
||||
except socket.timeout:
|
||||
_LOGGER.debug(f"[{self._device_id}] Connection timed out")
|
||||
MideaLogger.debug(f"Connection timed out", self._device_id)
|
||||
except socket.error:
|
||||
_LOGGER.debug(f"[{self._device_id}] Connection error")
|
||||
MideaLogger.debug(f"Connection error", self._device_id)
|
||||
except AuthException:
|
||||
_LOGGER.debug(f"[{self._device_id}] Authentication failed")
|
||||
MideaLogger.debug(f"Authentication failed", self._device_id)
|
||||
except ResponseException:
|
||||
_LOGGER.debug(f"[{self._device_id}] Unexpected response received")
|
||||
MideaLogger.debug(f"Unexpected response received", self._device_id)
|
||||
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:
|
||||
_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)}")
|
||||
self.device_connected(False)
|
||||
return False
|
||||
@@ -149,7 +199,7 @@ class MiedaDevice(threading.Thread):
|
||||
def authenticate(self):
|
||||
request = self._security.encode_8370(
|
||||
self._token, MSGTYPE_HANDSHAKE_REQUEST)
|
||||
_LOGGER.debug(f"[{self._device_id}] Handshaking")
|
||||
MideaLogger.debug(f"Handshaking")
|
||||
self._socket.send(request)
|
||||
response = self._socket.recv(512)
|
||||
if len(response) < 20:
|
||||
@@ -167,20 +217,21 @@ class MiedaDevice(threading.Thread):
|
||||
if self._socket is not None:
|
||||
self._socket.send(data)
|
||||
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):
|
||||
data = self._security.encode_8370(data, msg_type)
|
||||
self.send_message_v2(data)
|
||||
|
||||
def build_send(self, cmd):
|
||||
_LOGGER.debug(f"[{self._device_id}] Sending: {cmd}")
|
||||
MideaLogger.debug(f"Sending: {cmd}")
|
||||
bytes_cmd = bytes.fromhex(cmd)
|
||||
msg = PacketBuilder(self._device_id, bytes_cmd).finalize()
|
||||
self.send_message(msg)
|
||||
|
||||
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)
|
||||
|
||||
def parse_message(self, msg):
|
||||
@@ -202,10 +253,10 @@ class MiedaDevice(threading.Thread):
|
||||
cryptographic = message[40:-16]
|
||||
if payload_len % 16 == 0:
|
||||
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())
|
||||
_LOGGER.debug(f"[{self._device_id}] Decoded: {status}")
|
||||
MideaLogger.debug(f"Decoded: {status}")
|
||||
new_status = {}
|
||||
for single in status.keys():
|
||||
value = status.get(single)
|
||||
@@ -229,7 +280,7 @@ class MiedaDevice(threading.Thread):
|
||||
self._updates.append(update)
|
||||
|
||||
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:
|
||||
update(status)
|
||||
|
||||
@@ -241,17 +292,17 @@ class MiedaDevice(threading.Thread):
|
||||
def close(self):
|
||||
if self._is_run:
|
||||
self._is_run = False
|
||||
self._lua_runtime = None
|
||||
self.close_socket()
|
||||
|
||||
def close_socket(self):
|
||||
self._unsupported_protocol = []
|
||||
self._buffer = b""
|
||||
if self._socket:
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
|
||||
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.close_socket()
|
||||
|
||||
@@ -283,7 +334,7 @@ class MiedaDevice(threading.Thread):
|
||||
raise socket.error("Connection closed by peer")
|
||||
result = self.parse_message(msg)
|
||||
if result == ParseMessageResult.ERROR:
|
||||
_LOGGER.debug(f"[{self._device_id}] Message 'ERROR' received")
|
||||
MideaLogger.debug(f"Message 'ERROR' received")
|
||||
self.close_socket()
|
||||
break
|
||||
elif result == ParseMessageResult.SUCCESS:
|
||||
@@ -291,15 +342,15 @@ class MiedaDevice(threading.Thread):
|
||||
except socket.timeout:
|
||||
timeout_counter = timeout_counter + 1
|
||||
if timeout_counter >= 120:
|
||||
_LOGGER.debug(f"[{self._device_id}] Heartbeat timed out")
|
||||
MideaLogger.debug(f"Heartbeat timed out")
|
||||
self.close_socket()
|
||||
break
|
||||
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()
|
||||
break
|
||||
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)}")
|
||||
self.close_socket()
|
||||
break
|
||||
|
@@ -1,14 +1,13 @@
|
||||
import logging
|
||||
import socket
|
||||
import ifaddr
|
||||
from ipaddress import IPv4Network
|
||||
from .security import LocalSecurity
|
||||
from .logger import MideaLogger
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BROADCAST_MSG = bytearray([
|
||||
0x5a, 0x5a, 0x01, 0x11, 0x48, 0x00, 0x92, 0x00,
|
||||
@@ -34,7 +33,7 @@ DEVICE_INFO_MSG = bytearray([
|
||||
|
||||
|
||||
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:
|
||||
discover_type = []
|
||||
security = LocalSecurity()
|
||||
@@ -55,7 +54,7 @@ def discover(discover_type=None, ip_address=None):
|
||||
try:
|
||||
data, addr = sock.recvfrom(512)
|
||||
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 data[:2].hex() == "5a5a":
|
||||
protocol = 2
|
||||
@@ -70,7 +69,7 @@ def discover(discover_type=None, ip_address=None):
|
||||
continue
|
||||
encrypt_data = data[40:-16]
|
||||
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")
|
||||
device_type = ssid.split("_")[1]
|
||||
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:
|
||||
found_devices[device_id] = device
|
||||
_LOGGER.debug(f"Found a supported device: {device}")
|
||||
MideaLogger.debug(f"Found a supported device: {device}")
|
||||
else:
|
||||
_LOGGER.debug(f"Found a unsupported device: {device}")
|
||||
MideaLogger.debug(f"Found a unsupported device: {device}")
|
||||
except socket.timeout:
|
||||
break
|
||||
except socket.error as e:
|
||||
_LOGGER.debug(f"Socket error: {repr(e)}")
|
||||
MideaLogger.debug(f"Socket error: {repr(e)}")
|
||||
return found_devices
|
||||
|
||||
|
||||
@@ -147,15 +146,15 @@ def get_device_info(device_ip, device_port: int):
|
||||
sock.settimeout(8)
|
||||
device_address = (device_ip, device_port)
|
||||
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)
|
||||
response = sock.recv(512)
|
||||
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."
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
36
custom_components/midea_meiju_codec/core/logger.py
Normal file
36
custom_components/midea_meiju_codec/core/logger.py
Normal 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)
|
@@ -1,10 +1,7 @@
|
||||
import lupa
|
||||
import logging
|
||||
import threading
|
||||
import json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LuaRuntime:
|
||||
def __init__(self, file):
|
||||
@@ -27,17 +24,17 @@ class LuaRuntime:
|
||||
|
||||
|
||||
class MideaCodec(LuaRuntime):
|
||||
def __init__(self, file, sn=None, sub_type=None):
|
||||
def __init__(self, file, sn=None, subtype=None):
|
||||
super().__init__(file)
|
||||
self._sn = sn
|
||||
self._sub_type = sub_type
|
||||
self._subtype = subtype
|
||||
|
||||
def _build_base_dict(self):
|
||||
device_info ={}
|
||||
if self._sn is not None:
|
||||
device_info["deviceSN"] = self._sn
|
||||
if self._sub_type is not None:
|
||||
device_info["deviceSubType"] = self._sub_type
|
||||
if self._subtype is not None:
|
||||
device_info["deviceSubType"] = self._subtype
|
||||
base_dict = {
|
||||
"deviceinfo": device_info
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ class PacketBuilder:
|
||||
def __init__(self, device_id: int, command):
|
||||
self.command = None
|
||||
self.security = LocalSecurity()
|
||||
# aa20ac00000000000003418100ff03ff000200000000000000000000000006f274
|
||||
# Init the packet with the header data.
|
||||
self.packet = bytearray([
|
||||
# 2 bytes - StaicHeader
|
||||
|
@@ -1,20 +1,18 @@
|
||||
import hmac
|
||||
import logging
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from Crypto.Util.strxor import strxor
|
||||
from Crypto.Random import get_random_bytes
|
||||
from hashlib import md5, sha256
|
||||
from urllib.parse import urlparse
|
||||
import hmac
|
||||
import urllib
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MSGTYPE_HANDSHAKE_REQUEST = 0x0
|
||||
MSGTYPE_HANDSHAKE_RESPONSE = 0x1
|
||||
MSGTYPE_ENCRYPTED_RESPONSE = 0x3
|
||||
MSGTYPE_ENCRYPTED_REQUEST = 0x6
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudSecurity:
|
||||
|
||||
|
106
custom_components/midea_meiju_codec/device_map/device_mapping.py
Normal file
106
custom_components/midea_meiju_codec/device_map/device_mapping.py
Normal 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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/georgezhao2010/midea-meiju-codec/issues",
|
||||
"requirements": ["lupa>=2.0"],
|
||||
"version": "v0.0.2"
|
||||
"version": "v0.0.3"
|
||||
}
|
@@ -1,32 +1,43 @@
|
||||
from enum import IntEnum
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .core.logger import MideaLogger
|
||||
|
||||
|
||||
class Rationale(IntEnum):
|
||||
EQUALLY = 0
|
||||
GREATER = 1
|
||||
LESS = 2
|
||||
|
||||
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.register_update(self.update_state)
|
||||
self._entity_key = entity_key
|
||||
self._unique_id = f"{DOMAIN}.{self._device.device_id}_{entity_key}"
|
||||
self.entity_id = self._unique_id
|
||||
self._config = config
|
||||
self._device_name = self._device.device_name
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"manufacturer": "Midea",
|
||||
"model": f"{self._device.model} ({self._device.sn8})",
|
||||
self._attr_native_unit_of_measurement = self._config.get("unit")
|
||||
self._attr_device_class = self._config.get("device_class")
|
||||
self._attr_state_class = self._config.get("state_class")
|
||||
self._attr_unit_of_measurement = self._config.get("unit")
|
||||
self._attr_icon = self._config.get("icon")
|
||||
self._attr_unique_id = f"{DOMAIN}.{self._device.device_id}_{self._entity_key}"
|
||||
MideaLogger.debug(self._attr_unique_id)
|
||||
self._attr_device_info = {
|
||||
"manufacturer": "Midea" if manufacturer is None else manufacturer,
|
||||
"model": f"{self._device.model}",
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device_name
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self._unique_id
|
||||
name = self._config.get("name")
|
||||
if name is None:
|
||||
name = self._entity_key.replace("_", " ").title()
|
||||
self._attr_name = f"{self._device_name} {name}"
|
||||
self.entity_id = self._attr_unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -34,15 +45,52 @@ class MideaEntity(Entity):
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._device.get_attribute(self._entity_key)
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
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):
|
||||
if self._entity_key in status or "connected" in status:
|
||||
|
||||
try:
|
||||
self.schedule_update_ha_state()
|
||||
except Exception as e:
|
||||
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]
|
31
custom_components/midea_meiju_codec/sensor.py
Normal file
31
custom_components/midea_meiju_codec/sensor.py
Normal 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)
|
35
custom_components/midea_meiju_codec/switch.py
Normal file
35
custom_components/midea_meiju_codec/switch.py
Normal 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])
|
Reference in New Issue
Block a user