mirror of
https://github.com/sususweet/midea-meiju-codec.git
synced 2025-12-20 11:27:09 +00:00
v0.0.3
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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"),
|
"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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
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",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -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]
|
||||||
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