mirror of
https://github.com/sususweet/midea-meiju-codec.git
synced 2025-09-28 02:32:40 +00:00
Add online status
This commit is contained in:
@@ -9,6 +9,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from homeassistant.util.json import save_json
|
from homeassistant.util.json import save_json
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall
|
ServiceCall
|
||||||
@@ -26,9 +27,11 @@ from homeassistant.const import (
|
|||||||
CONF_DEVICE,
|
CONF_DEVICE,
|
||||||
CONF_ENTITIES
|
CONF_ENTITIES
|
||||||
)
|
)
|
||||||
|
|
||||||
from .core.logger import MideaLogger
|
from .core.logger import MideaLogger
|
||||||
from .core.device import MiedaDevice
|
from .core.device import MiedaDevice
|
||||||
from .data_coordinator import MideaDataUpdateCoordinator
|
from .data_coordinator import MideaDataUpdateCoordinator
|
||||||
|
from .core.cloud import get_midea_cloud
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DEVICES,
|
DEVICES,
|
||||||
@@ -39,20 +42,21 @@ from .const import (
|
|||||||
CONF_SN8,
|
CONF_SN8,
|
||||||
CONF_SN,
|
CONF_SN,
|
||||||
CONF_MODEL_NUMBER,
|
CONF_MODEL_NUMBER,
|
||||||
CONF_LUA_FILE
|
CONF_LUA_FILE, CONF_SERVERS
|
||||||
)
|
)
|
||||||
|
# 账号型:登录云端、获取设备列表,并为每台设备建立协调器(无本地控制)
|
||||||
|
from .const import CONF_PASSWORD as CONF_PASSWORD_KEY, CONF_SERVER as CONF_SERVER_KEY
|
||||||
|
|
||||||
ALL_PLATFORM = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.SENSOR,
|
# Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
# Platform.SWITCH,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.WATER_HEATER,
|
Platform.WATER_HEATER,
|
||||||
Platform.FAN
|
Platform.FAN
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_sn8_used(hass: HomeAssistant, sn8):
|
def get_sn8_used(hass: HomeAssistant, sn8):
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
count = 0
|
count = 0
|
||||||
@@ -175,83 +179,87 @@ async def async_setup(hass: HomeAssistant, config: ConfigType):
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
device_type = config_entry.data.get(CONF_TYPE)
|
device_type = config_entry.data.get(CONF_TYPE)
|
||||||
|
MideaLogger.debug(f"async_setup_entry type={device_type} data={config_entry.data}")
|
||||||
if device_type == CONF_ACCOUNT:
|
if device_type == CONF_ACCOUNT:
|
||||||
return True
|
account = config_entry.data.get(CONF_ACCOUNT)
|
||||||
name = config_entry.data.get(CONF_NAME)
|
password = config_entry.data.get(CONF_PASSWORD_KEY)
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
server = config_entry.data.get(CONF_SERVER_KEY)
|
||||||
device_type = config_entry.data.get(CONF_TYPE)
|
cloud_name = CONF_SERVERS.get(server)
|
||||||
token = config_entry.data.get(CONF_TOKEN)
|
cloud = get_midea_cloud(
|
||||||
key = config_entry.data.get(CONF_KEY)
|
cloud_name=cloud_name,
|
||||||
ip_address = config_entry.options.get(CONF_IP_ADDRESS, None)
|
session=async_get_clientsession(hass),
|
||||||
if not ip_address:
|
account=account,
|
||||||
ip_address = config_entry.data.get(CONF_IP_ADDRESS)
|
password=password,
|
||||||
refresh_interval = config_entry.options.get(CONF_REFRESH_INTERVAL)
|
|
||||||
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(CONF_MODEL_NUMBER)
|
|
||||||
sn = config_entry.data.get(CONF_SN)
|
|
||||||
sn8 = config_entry.data.get(CONF_SN8)
|
|
||||||
lua_file = config_entry.data.get(CONF_LUA_FILE)
|
|
||||||
if protocol == 3 and (key is None or key is None):
|
|
||||||
MideaLogger.error("For V3 devices, the key and the token is required.")
|
|
||||||
return False
|
|
||||||
device = MiedaDevice(
|
|
||||||
name=name,
|
|
||||||
device_id=device_id,
|
|
||||||
device_type=device_type,
|
|
||||||
ip_address=ip_address,
|
|
||||||
port=port,
|
|
||||||
token=token,
|
|
||||||
key=key,
|
|
||||||
protocol=protocol,
|
|
||||||
model=model,
|
|
||||||
subtype=subtype,
|
|
||||||
sn=sn,
|
|
||||||
sn8=sn8,
|
|
||||||
lua_file=lua_file,
|
|
||||||
)
|
)
|
||||||
if refresh_interval is not None:
|
if not cloud or not await cloud.login():
|
||||||
device.set_refresh_interval(refresh_interval)
|
MideaLogger.error("Midea cloud login failed")
|
||||||
device.open()
|
return False
|
||||||
if DOMAIN not in hass.data:
|
|
||||||
hass.data[DOMAIN] = {}
|
|
||||||
if DEVICES not in hass.data[DOMAIN]:
|
|
||||||
hass.data[DOMAIN][DEVICES] = {}
|
|
||||||
|
|
||||||
# Create data coordinator
|
# 拉取家庭与设备列表
|
||||||
|
appliances = None
|
||||||
|
try:
|
||||||
|
homes = await cloud.list_home()
|
||||||
|
if homes and len(homes) > 0:
|
||||||
|
first_home_id = list(homes.keys())[0]
|
||||||
|
appliances = await cloud.list_appliances(first_home_id)
|
||||||
|
else:
|
||||||
|
appliances = await cloud.list_appliances(None)
|
||||||
|
except Exception as e:
|
||||||
|
MideaLogger.error(f"Fetch appliances failed: {e}")
|
||||||
|
appliances = None
|
||||||
|
|
||||||
|
if appliances is None:
|
||||||
|
appliances = {}
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN].setdefault("accounts", {})
|
||||||
|
bucket = {"device_list": {}, "coordinator_map": {}}
|
||||||
|
|
||||||
|
# 为每台设备构建占位设备与协调器(不连接本地)
|
||||||
|
for appliance_code, info in appliances.items():
|
||||||
|
MideaLogger.debug(f"info={info} ")
|
||||||
|
try:
|
||||||
|
device = MiedaDevice(
|
||||||
|
name=info.get(CONF_NAME) or info.get("name"),
|
||||||
|
device_id=appliance_code,
|
||||||
|
device_type=info.get(CONF_TYPE) or info.get("type"),
|
||||||
|
ip_address=None,
|
||||||
|
port=None,
|
||||||
|
token=None,
|
||||||
|
key=None,
|
||||||
|
connected=info.get("online"),
|
||||||
|
protocol=info.get(CONF_PROTOCOL) or 2,
|
||||||
|
model=info.get(CONF_MODEL),
|
||||||
|
subtype=info.get(CONF_MODEL_NUMBER),
|
||||||
|
sn=info.get(CONF_SN) or info.get("sn"),
|
||||||
|
sn8=info.get(CONF_SN8) or info.get("sn8"),
|
||||||
|
lua_file=None,
|
||||||
|
)
|
||||||
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device)
|
coordinator = MideaDataUpdateCoordinator(hass, config_entry, device)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
bucket["device_list"][appliance_code] = info
|
||||||
|
bucket["coordinator_map"][appliance_code] = coordinator
|
||||||
|
except Exception as e:
|
||||||
|
MideaLogger.error(f"Init device failed: {appliance_code}, error: {e}")
|
||||||
|
|
||||||
hass.data[DOMAIN][DEVICES][device_id] = {}
|
hass.data[DOMAIN]["accounts"][config_entry.entry_id] = bucket
|
||||||
hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE] = device
|
|
||||||
hass.data[DOMAIN][DEVICES][device_id]["coordinator"] = coordinator
|
|
||||||
hass.data[DOMAIN][DEVICES][device_id][CONF_ENTITIES] = {}
|
|
||||||
|
|
||||||
config = load_device_config(hass, device_type, sn8)
|
hass.async_create_task(hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS))
|
||||||
if config is not None and len(config) > 0:
|
|
||||||
queries = config.get("queries")
|
|
||||||
if queries is not None and isinstance(queries, list):
|
|
||||||
device.set_queries(queries)
|
|
||||||
centralized = config.get("centralized")
|
|
||||||
if centralized is not None and isinstance(centralized, list):
|
|
||||||
device.set_centralized(centralized)
|
|
||||||
calculate = config.get("calculate")
|
|
||||||
if calculate is not None and isinstance(calculate, dict):
|
|
||||||
device.set_calculate(calculate)
|
|
||||||
hass.data[DOMAIN][DEVICES][device_id]["manufacturer"] = config.get("manufacturer")
|
|
||||||
hass.data[DOMAIN][DEVICES][device_id]["rationale"] = config.get("rationale")
|
|
||||||
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)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
||||||
|
device_type = config_entry.data.get(CONF_TYPE)
|
||||||
|
if device_type == CONF_ACCOUNT:
|
||||||
|
# 卸载平台并清理账号桶
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
try:
|
||||||
|
hass.data.get(DOMAIN, {}).get("accounts", {}).pop(config_entry.entry_id, None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return unload_ok
|
||||||
if device_id is not None:
|
if device_id is not None:
|
||||||
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
|
device: MiedaDevice = hass.data[DOMAIN][DEVICES][device_id][CONF_DEVICE]
|
||||||
if device is not None:
|
if device is not None:
|
||||||
@@ -259,7 +267,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||||||
lua_file = config_entry.data.get("lua_file")
|
lua_file = config_entry.data.get("lua_file")
|
||||||
os.remove(lua_file)
|
os.remove(lua_file)
|
||||||
remove_device_config(hass, device.sn8)
|
remove_device_config(hass, device.sn8)
|
||||||
device.close()
|
# device.close()
|
||||||
hass.data[DOMAIN][DEVICES].pop(device_id)
|
hass.data[DOMAIN][DEVICES].pop(device_id)
|
||||||
for platform in ALL_PLATFORM:
|
for platform in ALL_PLATFORM:
|
||||||
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
await hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||||
|
@@ -2,20 +2,14 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorDeviceClass
|
BinarySensorDeviceClass
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import Platform
|
||||||
Platform,
|
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_ENTITIES, CONF_DEVICE
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entity import MideaEntity
|
from .midea_entity import MideaEntity
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -24,19 +18,29 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up binary sensor entities for Midea devices."""
|
"""Set up binary sensor entities for Midea devices."""
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device_data = hass.data[DOMAIN][DEVICES][device_id]
|
if not account_bucket:
|
||||||
coordinator = device_data.get("coordinator")
|
async_add_entities([])
|
||||||
device = device_data.get(CONF_DEVICE)
|
return
|
||||||
manufacturer = device_data.get("manufacturer")
|
device_list = account_bucket.get("device_list", {})
|
||||||
rationale = device_data.get("rationale")
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.BINARY_SENSOR, {})
|
|
||||||
|
|
||||||
devs = [MideaDeviceStatusSensorEntity(coordinator, device, manufacturer, rationale, "Status", {})]
|
devs = []
|
||||||
if entities:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.BINARY_SENSOR, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
# 连接状态实体
|
||||||
|
if coordinator and device:
|
||||||
|
devs.append(MideaDeviceStatusSensorEntity(coordinator, device, manufacturer, rationale, "Status", {}))
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
devs.append(MideaBinarySensorEntity(
|
devs.append(MideaBinarySensorEntity(
|
||||||
coordinator, device, manufacturer, rationale, entity_key, config
|
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||||
))
|
))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
@@ -6,20 +6,16 @@ from homeassistant.components.climate import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
Platform,
|
Platform,
|
||||||
CONF_DEVICE_ID,
|
ATTR_TEMPERATURE,
|
||||||
CONF_ENTITIES,
|
|
||||||
ATTR_TEMPERATURE, CONF_DEVICE
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entity import MideaEntity
|
from .midea_entity import MideaEntity
|
||||||
from .midea_entities import Rationale
|
from .midea_entities import Rationale
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -28,19 +24,27 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up climate entities for Midea devices."""
|
"""Set up climate entities for Midea devices."""
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
# 账号型 entry:从 __init__ 写入的 accounts 桶加载设备和协调器
|
||||||
device_data = hass.data[DOMAIN][DEVICES][device_id]
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
coordinator = device_data.get("coordinator")
|
if not account_bucket:
|
||||||
device = device_data.get(CONF_DEVICE)
|
async_add_entities([])
|
||||||
manufacturer = device_data.get("manufacturer")
|
return
|
||||||
rationale = device_data.get("rationale")
|
device_list = account_bucket.get("device_list", {})
|
||||||
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.CLIMATE, {})
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.CLIMATE, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
devs.append(MideaClimateEntity(
|
devs.append(MideaClimateEntity(
|
||||||
coordinator, device, manufacturer, rationale, entity_key, config
|
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||||
))
|
))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
@@ -81,8 +85,8 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
if self._key_preset_modes is not None:
|
if self._key_preset_modes is not None:
|
||||||
features |= ClimateEntityFeature.PRESET_MODE
|
features |= ClimateEntityFeature.PRESET_MODE
|
||||||
if self._key_aux_heat is not None:
|
# if self._key_aux_heat is not None:
|
||||||
features |= ClimateEntityFeature.AUX_HEAT
|
# features |= ClimateEntityFeature.AUX_HEAT
|
||||||
if self._key_swing_modes is not None:
|
if self._key_swing_modes is not None:
|
||||||
features |= ClimateEntityFeature.SWING_MODE
|
features |= ClimateEntityFeature.SWING_MODE
|
||||||
if self._key_fan_modes is not None:
|
if self._key_fan_modes is not None:
|
||||||
@@ -227,7 +231,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
return
|
return
|
||||||
await self.async_set_attribute(key, value)
|
await self.async_set_attribute(key, value)
|
||||||
|
|
||||||
def _dict_get_selected(self, dict_config, rationale=Rationale.EQUAL):
|
def _dict_get_selected(self, dict_config, rationale=Rationale.EQUALLY):
|
||||||
"""Get selected value from dictionary configuration."""
|
"""Get selected value from dictionary configuration."""
|
||||||
if dict_config is None:
|
if dict_config is None:
|
||||||
return None
|
return None
|
||||||
@@ -238,7 +242,7 @@ class MideaClimateEntity(MideaEntity, ClimateEntity):
|
|||||||
match = True
|
match = True
|
||||||
for attr_key, attr_value in config.items():
|
for attr_key, attr_value in config.items():
|
||||||
device_value = self.device_attributes.get(attr_key)
|
device_value = self.device_attributes.get(attr_key)
|
||||||
if rationale == Rationale.EQUAL:
|
if rationale == Rationale.EQUALLY:
|
||||||
if device_value != attr_value:
|
if device_value != attr_value:
|
||||||
match = False
|
match = False
|
||||||
break
|
break
|
||||||
|
@@ -1,293 +1,68 @@
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import logging
|
import logging
|
||||||
import os
|
from typing import Any
|
||||||
import ipaddress
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.config_entries import ConfigFlowResult
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_PORT,
|
|
||||||
CONF_MODEL,
|
|
||||||
CONF_IP_ADDRESS,
|
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_PROTOCOL,
|
|
||||||
CONF_TOKEN,
|
|
||||||
CONF_NAME
|
|
||||||
)
|
)
|
||||||
from . import remove_device_config, load_device_config
|
|
||||||
from .core.cloud import get_midea_cloud
|
|
||||||
from .core.discover import discover
|
|
||||||
from .core.device import MiedaDevice
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
|
||||||
CONF_REFRESH_INTERVAL,
|
|
||||||
STORAGE_PATH,
|
|
||||||
CONF_ACCOUNT,
|
CONF_ACCOUNT,
|
||||||
CONF_SERVER,
|
CONF_PASSWORD,
|
||||||
CONF_HOME,
|
DOMAIN,
|
||||||
CONF_KEY,
|
CONF_SERVER, CONF_SERVERS
|
||||||
CONF_SN8,
|
|
||||||
CONF_SN,
|
|
||||||
CONF_MODEL_NUMBER,
|
|
||||||
CONF_LUA_FILE
|
|
||||||
)
|
)
|
||||||
|
from .core.cloud import get_midea_cloud
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
servers = {
|
|
||||||
1: "MSmartHome",
|
|
||||||
2: "美的美居",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
_session = None
|
_session = None
|
||||||
_cloud = None
|
|
||||||
_current_home = None
|
|
||||||
_device_list = {}
|
|
||||||
_device = None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry):
|
def async_get_options_flow(config_entry):
|
||||||
return OptionsFlowHandler(config_entry)
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
def _get_configured_account(self):
|
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||||
for entry in self._async_current_entries():
|
errors: dict[str, str] = {}
|
||||||
if entry.data.get(CONF_TYPE) == CONF_ACCOUNT:
|
|
||||||
return entry.data.get(CONF_ACCOUNT), entry.data.get(CONF_PASSWORD), entry.data.get(CONF_SERVER)
|
|
||||||
return None, None, None
|
|
||||||
|
|
||||||
def _device_configured(self, device_id):
|
|
||||||
for entry in self._async_current_entries():
|
|
||||||
if device_id == entry.data.get(CONF_DEVICE_ID):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_valid_ip_address(ip_address):
|
|
||||||
try:
|
|
||||||
ipaddress.ip_address(ip_address)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None, error=None):
|
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
self._session = async_create_clientsession(self.hass)
|
self._session = async_create_clientsession(self.hass)
|
||||||
account, password, server = self._get_configured_account()
|
|
||||||
if account is not None and password is not None:
|
|
||||||
if self._cloud is None:
|
|
||||||
self._cloud = get_midea_cloud(
|
|
||||||
session=self._session,
|
|
||||||
cloud_name=servers[server],
|
|
||||||
account=account,
|
|
||||||
password=password
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
if await self._cloud.login():
|
|
||||||
return await self.async_step_home()
|
|
||||||
else:
|
|
||||||
return await self.async_step_user(error="account_invalid")
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error(f"Login error: {e}")
|
|
||||||
return await self.async_step_user(error="login_failed")
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if self._cloud is None:
|
cloud = get_midea_cloud(
|
||||||
self._cloud = get_midea_cloud(
|
|
||||||
session=self._session,
|
session=self._session,
|
||||||
cloud_name=servers[user_input[CONF_SERVER]],
|
cloud_name=CONF_SERVERS[user_input[CONF_SERVER]],
|
||||||
account=user_input[CONF_ACCOUNT],
|
account=user_input[CONF_ACCOUNT],
|
||||||
password=user_input[CONF_PASSWORD]
|
password=user_input[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if await self._cloud.login():
|
if await cloud.login():
|
||||||
|
await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"{user_input[CONF_ACCOUNT]}",
|
title=user_input[CONF_ACCOUNT],
|
||||||
data={
|
data={
|
||||||
CONF_TYPE: CONF_ACCOUNT,
|
CONF_TYPE: CONF_ACCOUNT,
|
||||||
CONF_ACCOUNT: user_input[CONF_ACCOUNT],
|
CONF_ACCOUNT: user_input[CONF_ACCOUNT],
|
||||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
CONF_SERVER: user_input[CONF_SERVER]
|
CONF_SERVER: user_input[CONF_SERVER]
|
||||||
})
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._cloud = None
|
errors["base"] = "login_failed"
|
||||||
return await self.async_step_user(error="login_failed")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error(f"Login error: {e}")
|
_LOGGER.exception("Login error: %s", e)
|
||||||
self._cloud = None
|
errors["base"] = "login_failed"
|
||||||
return await self.async_step_user(error="login_failed")
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_ACCOUNT): str,
|
vol.Required(CONF_ACCOUNT): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Required(CONF_SERVER, default=1): vol.In(servers)
|
vol.Required(CONF_SERVER, default=1): vol.In(CONF_SERVERS)
|
||||||
}),
|
}),
|
||||||
errors={"base": error} if error else None
|
errors=errors,
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_home(self, user_input=None, error=None):
|
|
||||||
if user_input is not None:
|
|
||||||
self._current_home = user_input[CONF_HOME]
|
|
||||||
return await self.async_step_device()
|
|
||||||
homes = await self._cloud.list_home()
|
|
||||||
if homes is None or len(homes) == 0:
|
|
||||||
return await self.async_step_device(error="no_home")
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="home",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required(CONF_HOME, default=list(homes.keys())[0]):
|
|
||||||
vol.In(homes),
|
|
||||||
}),
|
|
||||||
errors={"base": error} if error else None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_device(self, user_input=None, error=None):
|
|
||||||
if user_input is not None:
|
|
||||||
# 下载lua
|
|
||||||
# 本地尝试连接设备
|
|
||||||
self._device = self._device_list[user_input[CONF_DEVICE_ID]]
|
|
||||||
if self._device.get("online") is not True:
|
|
||||||
return await self.async_step_device(error="offline_error")
|
|
||||||
return await self.async_step_discover()
|
|
||||||
appliances = await self._cloud.list_appliances(self._current_home)
|
|
||||||
self._device_list = {}
|
|
||||||
device_list = {}
|
|
||||||
for appliance_code, appliance_info in appliances.items():
|
|
||||||
if not self._device_configured(appliance_code):
|
|
||||||
try:
|
|
||||||
model_number = int(appliance_info.get("model_number")) if appliance_info.get("model_number") is not None else 0
|
|
||||||
except ValueError:
|
|
||||||
model_number = 0
|
|
||||||
self._device_list[appliance_code] = {
|
|
||||||
CONF_DEVICE_ID: appliance_code,
|
|
||||||
CONF_NAME: appliance_info.get("name"),
|
|
||||||
CONF_TYPE: appliance_info.get("type"),
|
|
||||||
CONF_SN8: appliance_info.get("sn8", "00000000"),
|
|
||||||
CONF_SN: appliance_info.get("sn"),
|
|
||||||
CONF_MODEL: appliance_info.get("model", "0"),
|
|
||||||
CONF_MODEL_NUMBER: model_number,
|
|
||||||
"manufacturer_code": appliance_info.get("manufacturer_code","0000"),
|
|
||||||
"online": appliance_info.get("online")
|
|
||||||
}
|
|
||||||
device_list[appliance_code] = \
|
|
||||||
f"{appliance_info.get('name')} ({'online' if appliance_info.get('online') is True else 'offline'})"
|
|
||||||
|
|
||||||
if len(self._device_list) == 0:
|
|
||||||
return await self.async_step_device(error="no_new_devices")
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="device",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required(CONF_DEVICE_ID, default=list(device_list.keys())[0]):
|
|
||||||
vol.In(device_list),
|
|
||||||
}),
|
|
||||||
errors={"base": error} if error else None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_discover(self, user_input=None, error=None):
|
|
||||||
if user_input is not None:
|
|
||||||
if user_input[CONF_IP_ADDRESS] == "auto" or self._is_valid_ip_address(user_input[CONF_IP_ADDRESS]):
|
|
||||||
ip_address = None
|
|
||||||
if self._is_valid_ip_address(user_input[CONF_IP_ADDRESS]):
|
|
||||||
ip_address = user_input[CONF_IP_ADDRESS]
|
|
||||||
discover_devices = discover([self._device[CONF_TYPE]], ip_address)
|
|
||||||
_LOGGER.debug(discover_devices)
|
|
||||||
if discover_devices is None or len(discover_devices) == 0:
|
|
||||||
return await self.async_step_discover(error="discover_failed")
|
|
||||||
current_device = discover_devices.get(self._device[CONF_DEVICE_ID])
|
|
||||||
if current_device is None:
|
|
||||||
return await self.async_step_discover(error="discover_failed")
|
|
||||||
os.makedirs(self.hass.config.path(STORAGE_PATH), exist_ok=True)
|
|
||||||
path = self.hass.config.path(STORAGE_PATH)
|
|
||||||
file = await self._cloud.download_lua(
|
|
||||||
path=path,
|
|
||||||
device_type=self._device[CONF_TYPE],
|
|
||||||
sn=self._device[CONF_SN],
|
|
||||||
model_number=self._device[CONF_MODEL_NUMBER],
|
|
||||||
manufacturer_code=self._device["manufacturer_code"]
|
|
||||||
)
|
|
||||||
if file is None:
|
|
||||||
return await self.async_step_discover(error="download_lua_failed")
|
|
||||||
use_token = None
|
|
||||||
use_key = None
|
|
||||||
connected = False
|
|
||||||
if current_device.get(CONF_PROTOCOL) == 3:
|
|
||||||
keys = await self._cloud.get_keys(self._device.get(CONF_DEVICE_ID))
|
|
||||||
for method, key in keys.items():
|
|
||||||
dm = MiedaDevice(
|
|
||||||
name="",
|
|
||||||
device_id=self._device.get(CONF_DEVICE_ID),
|
|
||||||
device_type=current_device.get(CONF_TYPE),
|
|
||||||
ip_address=current_device.get(CONF_IP_ADDRESS),
|
|
||||||
port=current_device.get(CONF_PORT),
|
|
||||||
token=key["token"],
|
|
||||||
key=key["key"],
|
|
||||||
protocol=3,
|
|
||||||
model=None,
|
|
||||||
subtype = None,
|
|
||||||
sn=None,
|
|
||||||
sn8=None,
|
|
||||||
lua_file=None
|
|
||||||
)
|
|
||||||
_LOGGER.debug(
|
|
||||||
f"Successful to take token and key, token: {key['token']},"
|
|
||||||
f" key: { key['key']}, method: {method}"
|
|
||||||
)
|
|
||||||
if dm.connect():
|
|
||||||
use_token = key["token"]
|
|
||||||
use_key = key["key"]
|
|
||||||
dm.disconnect()
|
|
||||||
connected = True
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
dm = MiedaDevice(
|
|
||||||
name=self._device.get("name"),
|
|
||||||
device_id=self._device.get("device_id"),
|
|
||||||
device_type=current_device.get(CONF_TYPE),
|
|
||||||
ip_address=current_device.get(CONF_IP_ADDRESS),
|
|
||||||
port=current_device.get(CONF_PORT),
|
|
||||||
token=None,
|
|
||||||
key=None,
|
|
||||||
protocol=2,
|
|
||||||
model=None,
|
|
||||||
subtype=None,
|
|
||||||
sn=None,
|
|
||||||
sn8=None,
|
|
||||||
lua_file=None
|
|
||||||
)
|
|
||||||
if dm.connect():
|
|
||||||
dm.disconnect()
|
|
||||||
connected = True
|
|
||||||
if not connected:
|
|
||||||
return await self.async_step_discover(error="connect_error")
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self._device.get("name"),
|
|
||||||
data={
|
|
||||||
CONF_NAME: self._device.get(CONF_NAME),
|
|
||||||
CONF_DEVICE_ID: self._device.get(CONF_DEVICE_ID),
|
|
||||||
CONF_TYPE: current_device.get(CONF_TYPE),
|
|
||||||
CONF_PROTOCOL: current_device.get(CONF_PROTOCOL),
|
|
||||||
CONF_IP_ADDRESS: current_device.get(CONF_IP_ADDRESS),
|
|
||||||
CONF_PORT: current_device.get(CONF_PORT),
|
|
||||||
CONF_TOKEN: use_token,
|
|
||||||
CONF_KEY: use_key,
|
|
||||||
CONF_MODEL: self._device.get(CONF_MODEL),
|
|
||||||
CONF_MODEL_NUMBER: self._device.get(CONF_MODEL_NUMBER),
|
|
||||||
CONF_SN: self._device.get(CONF_SN),
|
|
||||||
CONF_SN8: self._device.get(CONF_SN8),
|
|
||||||
CONF_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, default="auto"): str
|
|
||||||
}),
|
|
||||||
errors={"base": error} if error else None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -296,64 +71,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None, error=None):
|
async def async_step_init(self, user_input=None, error=None):
|
||||||
if self._config_entry.data.get(CONF_TYPE) == CONF_ACCOUNT:
|
# 账号型条目不支持配置项
|
||||||
return self.async_abort(reason="account_unsupport_config")
|
return self.async_abort(reason="account_unsupport_config")
|
||||||
if user_input is not None:
|
# 不再提供任何可配置项
|
||||||
if user_input.get("option") == 1:
|
return self.async_abort(reason="account_unsupport_config")
|
||||||
return await self.async_step_configure()
|
# 不提供 reset/configure 等选项步骤
|
||||||
else:
|
|
||||||
return await self.async_step_reset()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="init",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required("option", default=1):
|
|
||||||
vol.In({1: "Options", 2: "Reset device configuration"})
|
|
||||||
|
|
||||||
}),
|
|
||||||
errors={"base": error} if error else None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reset(self, user_input=None):
|
|
||||||
if user_input is not None:
|
|
||||||
if user_input["check"]:
|
|
||||||
remove_device_config(self.hass, self._config_entry.data.get(CONF_SN8))
|
|
||||||
load_device_config(
|
|
||||||
self.hass,
|
|
||||||
self._config_entry.data.get(CONF_TYPE),
|
|
||||||
self._config_entry.data.get(CONF_SN8))
|
|
||||||
return self.async_abort(reason="reset_success")
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reset",
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required("check", default=False): bool
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_configure(self, user_input=None):
|
|
||||||
if user_input is not None:
|
|
||||||
return self.async_create_entry(title="", data=user_input)
|
|
||||||
ip_address = self._config_entry.options.get(
|
|
||||||
CONF_IP_ADDRESS, None
|
|
||||||
)
|
|
||||||
if ip_address is None:
|
|
||||||
ip_address = self._config_entry.data.get(
|
|
||||||
CONF_IP_ADDRESS, None
|
|
||||||
)
|
|
||||||
refresh_interval = self._config_entry.options.get(
|
|
||||||
CONF_REFRESH_INTERVAL, 30
|
|
||||||
)
|
|
||||||
data_schema = vol.Schema({
|
|
||||||
vol.Required(
|
|
||||||
CONF_IP_ADDRESS,
|
|
||||||
default=ip_address
|
|
||||||
): str,
|
|
||||||
vol.Required(
|
|
||||||
CONF_REFRESH_INTERVAL,
|
|
||||||
default=refresh_interval
|
|
||||||
): int
|
|
||||||
})
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="configure",
|
|
||||||
data_schema=data_schema
|
|
||||||
)
|
|
File diff suppressed because one or more lines are too long
@@ -96,9 +96,10 @@ class MideaCloud:
|
|||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
print(response)
|
||||||
if int(response["code"]) == 0 and "data" in response:
|
if int(response["code"]) == 0 and "data" in response:
|
||||||
return response["data"]
|
return response["data"]
|
||||||
print(response)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _get_login_id(self) -> str | None:
|
async def _get_login_id(self) -> str | None:
|
||||||
|
@@ -32,13 +32,14 @@ class MiedaDevice(threading.Thread):
|
|||||||
name: str,
|
name: str,
|
||||||
device_id: int,
|
device_id: int,
|
||||||
device_type: int,
|
device_type: int,
|
||||||
ip_address: str,
|
ip_address: str | None,
|
||||||
port: int,
|
port: int | None,
|
||||||
token: str | None,
|
token: str | None,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
protocol: int,
|
protocol: int,
|
||||||
model: str | None,
|
model: str | None,
|
||||||
subtype: int | None,
|
subtype: int | None,
|
||||||
|
connected: bool,
|
||||||
sn: str | None,
|
sn: str | None,
|
||||||
sn8: str | None,
|
sn8: str | None,
|
||||||
lua_file: str | None):
|
lua_file: str | None):
|
||||||
@@ -68,7 +69,7 @@ class MiedaDevice(threading.Thread):
|
|||||||
}
|
}
|
||||||
self._refresh_interval = 30
|
self._refresh_interval = 30
|
||||||
self._heartbeat_interval = 10
|
self._heartbeat_interval = 10
|
||||||
self._connected = False
|
self._device_connected(connected)
|
||||||
self._queries = [{}]
|
self._queries = [{}]
|
||||||
self._centralized = []
|
self._centralized = []
|
||||||
self._calculate_get = []
|
self._calculate_get = []
|
||||||
@@ -170,45 +171,6 @@ class MiedaDevice(threading.Thread):
|
|||||||
def register_update(self, update):
|
def register_update(self, update):
|
||||||
self._updates.append(update)
|
self._updates.append(update)
|
||||||
|
|
||||||
def connect(self, refresh=False):
|
|
||||||
try:
|
|
||||||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self._socket.settimeout(10)
|
|
||||||
MideaLogger.debug(f"Connecting to {self._ip_address}:{self._port}", self._device_id)
|
|
||||||
self._socket.connect((self._ip_address, self._port))
|
|
||||||
MideaLogger.debug(f"Connected", self._device_id)
|
|
||||||
if self._protocol == 3:
|
|
||||||
self._authenticate()
|
|
||||||
MideaLogger.debug(f"Authentication success", self._device_id)
|
|
||||||
self._device_connected(True)
|
|
||||||
if refresh:
|
|
||||||
self._refresh_status()
|
|
||||||
return True
|
|
||||||
except socket.timeout:
|
|
||||||
MideaLogger.debug(f"Connection timed out", self._device_id)
|
|
||||||
except socket.error:
|
|
||||||
MideaLogger.debug(f"Connection error", self._device_id)
|
|
||||||
except AuthException:
|
|
||||||
MideaLogger.debug(f"Authentication failed", self._device_id)
|
|
||||||
except ResponseException:
|
|
||||||
MideaLogger.debug(f"Unexpected response received", self._device_id)
|
|
||||||
except RefreshFailed:
|
|
||||||
MideaLogger.debug(f"Refresh status is timed out", self._device_id)
|
|
||||||
except Exception as e:
|
|
||||||
MideaLogger.error(f"Unknown error: {e.__traceback__.tb_frame.f_globals['__file__']}, "
|
|
||||||
f"{e.__traceback__.tb_lineno}, {repr(e)}")
|
|
||||||
if refresh:
|
|
||||||
self._device_connected(False)
|
|
||||||
self._socket = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
self._buffer = b""
|
|
||||||
if self._socket:
|
|
||||||
self._socket.close()
|
|
||||||
self._socket = None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fetch_v2_message(msg):
|
def _fetch_v2_message(msg):
|
||||||
result = []
|
result = []
|
||||||
@@ -329,7 +291,7 @@ class MiedaDevice(threading.Thread):
|
|||||||
if not connected:
|
if not connected:
|
||||||
MideaLogger.warning(f"Device {self._device_id} disconnected", self._device_id)
|
MideaLogger.warning(f"Device {self._device_id} disconnected", self._device_id)
|
||||||
else:
|
else:
|
||||||
MideaLogger.info(f"Device {self._device_id} connected", self._device_id)
|
MideaLogger.debug(f"Device {self._device_id} connected", self._device_id)
|
||||||
self._update_all(status)
|
self._update_all(status)
|
||||||
|
|
||||||
def _update_all(self, status):
|
def _update_all(self, status):
|
||||||
@@ -337,64 +299,64 @@ class MiedaDevice(threading.Thread):
|
|||||||
for update in self._updates:
|
for update in self._updates:
|
||||||
update(status)
|
update(status)
|
||||||
|
|
||||||
def open(self):
|
# def open(self):
|
||||||
if not self._is_run:
|
# if not self._is_run:
|
||||||
self._is_run = True
|
# self._is_run = True
|
||||||
threading.Thread.start(self)
|
# threading.Thread.start(self)
|
||||||
|
#
|
||||||
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._lua_runtime = None
|
||||||
self.disconnect()
|
# self.disconnect()
|
||||||
|
#
|
||||||
def run(self):
|
# def run(self):
|
||||||
while self._is_run:
|
# while self._is_run:
|
||||||
while self._socket is None:
|
# while self._socket is None:
|
||||||
if self.connect(refresh=True) is False:
|
# if self.connect(refresh=True) is False:
|
||||||
if not self._is_run:
|
# if not self._is_run:
|
||||||
return
|
# return
|
||||||
self.disconnect()
|
# self.disconnect()
|
||||||
time.sleep(5)
|
# time.sleep(5)
|
||||||
timeout_counter = 0
|
# timeout_counter = 0
|
||||||
start = time.time()
|
# start = time.time()
|
||||||
previous_refresh = start
|
# previous_refresh = start
|
||||||
previous_heartbeat = start
|
# previous_heartbeat = start
|
||||||
self._socket.settimeout(1)
|
# self._socket.settimeout(1)
|
||||||
while True:
|
# while True:
|
||||||
try:
|
# try:
|
||||||
now = time.time()
|
# now = time.time()
|
||||||
if 0 < self._refresh_interval <= now - previous_refresh:
|
# if 0 < self._refresh_interval <= now - previous_refresh:
|
||||||
self._refresh_status()
|
# self._refresh_status()
|
||||||
previous_refresh = now
|
# previous_refresh = now
|
||||||
if now - previous_heartbeat >= self._heartbeat_interval:
|
# if now - previous_heartbeat >= self._heartbeat_interval:
|
||||||
self._send_heartbeat()
|
# self._send_heartbeat()
|
||||||
previous_heartbeat = now
|
# previous_heartbeat = now
|
||||||
msg = self._socket.recv(512)
|
# msg = self._socket.recv(512)
|
||||||
msg_len = len(msg)
|
# msg_len = len(msg)
|
||||||
if msg_len == 0:
|
# if msg_len == 0:
|
||||||
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:
|
||||||
MideaLogger.debug(f"Message 'ERROR' received")
|
# MideaLogger.debug(f"Message 'ERROR' received")
|
||||||
self.disconnect()
|
# self.disconnect()
|
||||||
break
|
# break
|
||||||
elif result == ParseMessageResult.SUCCESS:
|
# elif result == ParseMessageResult.SUCCESS:
|
||||||
timeout_counter = 0
|
# timeout_counter = 0
|
||||||
except socket.timeout:
|
# except socket.timeout:
|
||||||
timeout_counter = timeout_counter + 1
|
# timeout_counter = timeout_counter + 1
|
||||||
if timeout_counter >= 120:
|
# if timeout_counter >= 120:
|
||||||
MideaLogger.debug(f"Heartbeat timed out")
|
# MideaLogger.debug(f"Heartbeat timed out")
|
||||||
self.disconnect()
|
# self.disconnect()
|
||||||
break
|
# break
|
||||||
except socket.error as e:
|
# except socket.error as e:
|
||||||
MideaLogger.debug(f"Socket error {repr(e)}")
|
# MideaLogger.debug(f"Socket error {repr(e)}")
|
||||||
self.disconnect()
|
# self.disconnect()
|
||||||
break
|
# break
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
MideaLogger.error(f"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.disconnect()
|
# self.disconnect()
|
||||||
break
|
# break
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from homeassistant.const import *
|
from homeassistant.const import Platform, UnitOfTemperature, PRECISION_HALVES
|
||||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||||
# from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
# from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
from homeassistant.components.switch import SwitchDeviceClass
|
from homeassistant.components.switch import SwitchDeviceClass
|
||||||
@@ -56,7 +56,7 @@ DEVICE_MAPPING = {
|
|||||||
"aux_heat": "ptc",
|
"aux_heat": "ptc",
|
||||||
"min_temp": 17,
|
"min_temp": 17,
|
||||||
"max_temp": 30,
|
"max_temp": 30,
|
||||||
"temperature_unit": TEMP_CELSIUS,
|
"temperature_unit": UnitOfTemperature.CELSIUS,
|
||||||
"precision": PRECISION_HALVES,
|
"precision": PRECISION_HALVES,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from homeassistant.const import *
|
from homeassistant.const import Platform, UnitOfElectricPotential, UnitOfTemperature, UnitOfTime
|
||||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
from homeassistant.const import *
|
from homeassistant.const import Platform, UnitOfTemperature
|
||||||
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
from homeassistant.components.sensor import SensorStateClass, SensorDeviceClass
|
||||||
|
from homeassistant.components.climate.const import PRECISION_HALVES
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
from homeassistant.components.switch import SwitchDeviceClass
|
from homeassistant.components.switch import SwitchDeviceClass
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ DEVICE_MAPPING = {
|
|||||||
"aux_heat": "ptc",
|
"aux_heat": "ptc",
|
||||||
"min_temp": 17,
|
"min_temp": 17,
|
||||||
"max_temp": 30,
|
"max_temp": 30,
|
||||||
"temperature_unit": TEMP_CELSIUS,
|
"temperature_unit": UnitOfTemperature.CELSIUS,
|
||||||
"precision": PRECISION_HALVES,
|
"precision": PRECISION_HALVES,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,28 +1,30 @@
|
|||||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
from homeassistant.const import (
|
from homeassistant.const import Platform
|
||||||
Platform,
|
from .const import DOMAIN
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_DEVICE,
|
|
||||||
CONF_ENTITIES,
|
|
||||||
)
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entities import MideaEntity
|
from .midea_entities import MideaEntity
|
||||||
from .core.logger import MideaLogger
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
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)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
|
if not account_bucket:
|
||||||
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
|
async_add_entities([])
|
||||||
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale")
|
return
|
||||||
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.FAN)
|
device_list = account_bucket.get("device_list", {})
|
||||||
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities is not None:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
devs.append(MideaFanEntity(device, manufacturer, rationale, entity_key, config))
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.FAN, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
|
devs.append(MideaFanEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .core.logger import MideaLogger
|
||||||
from .data_coordinator import MideaDataUpdateCoordinator
|
from .data_coordinator import MideaDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -78,6 +79,7 @@ class MideaEntity(CoordinatorEntity[MideaDataUpdateCoordinator], Entity):
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
|
MideaLogger.debug(f"available available={self.coordinator.data} ")
|
||||||
return self.coordinator.data.available
|
return self.coordinator.data.available
|
||||||
|
|
||||||
async def _publish_command(self) -> None:
|
async def _publish_command(self) -> None:
|
||||||
|
@@ -1,27 +1,30 @@
|
|||||||
from homeassistant.components.select import SelectEntity
|
from homeassistant.components.select import SelectEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import Platform
|
||||||
Platform,
|
from .const import DOMAIN
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_DEVICE,
|
|
||||||
CONF_ENTITIES,
|
|
||||||
)
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entities import MideaEntity
|
from .midea_entities import MideaEntity
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
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)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
|
if not account_bucket:
|
||||||
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
|
async_add_entities([])
|
||||||
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale")
|
return
|
||||||
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.SELECT)
|
device_list = account_bucket.get("device_list", {})
|
||||||
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities is not None:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
devs.append(MideaSelectEntity(device, manufacturer, rationale, entity_key, config))
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.SELECT, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
|
devs.append(MideaSelectEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,18 +1,12 @@
|
|||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import Platform
|
||||||
Platform,
|
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_ENTITIES, CONF_DEVICE
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entity import MideaEntity
|
from .midea_entity import MideaEntity
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -21,19 +15,26 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up sensor entities for Midea devices."""
|
"""Set up sensor entities for Midea devices."""
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device_data = hass.data[DOMAIN][DEVICES][device_id]
|
if not account_bucket:
|
||||||
coordinator = device_data.get("coordinator")
|
async_add_entities([])
|
||||||
device = device_data.get(CONF_DEVICE)
|
return
|
||||||
manufacturer = device_data.get("manufacturer")
|
device_list = account_bucket.get("device_list", {})
|
||||||
rationale = device_data.get("rationale")
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SENSOR, {})
|
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.SENSOR, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
devs.append(MideaSensorEntity(
|
devs.append(MideaSensorEntity(
|
||||||
coordinator, device, manufacturer, rationale, entity_key, config
|
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||||
))
|
))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import Platform
|
||||||
Platform,
|
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_ENTITIES, CONF_DEVICE,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
DOMAIN,
|
from .core.logger import MideaLogger
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entity import MideaEntity
|
from .midea_entity import MideaEntity
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -21,19 +16,26 @@ async def async_setup_entry(
|
|||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up switch entities for Midea devices."""
|
"""Set up switch entities for Midea devices."""
|
||||||
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device_data = hass.data[DOMAIN][DEVICES][device_id]
|
if not account_bucket:
|
||||||
coordinator = device_data.get("coordinator")
|
async_add_entities([])
|
||||||
device = device_data.get(CONF_DEVICE)
|
return
|
||||||
manufacturer = device_data.get("manufacturer")
|
device_list = account_bucket.get("device_list", {})
|
||||||
rationale = device_data.get("rationale")
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
entities = device_data.get(CONF_ENTITIES, {}).get(Platform.SWITCH, {})
|
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.SWITCH, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
devs.append(MideaSwitchEntity(
|
devs.append(MideaSwitchEntity(
|
||||||
coordinator, device, manufacturer, rationale, entity_key, config
|
coordinator, device, manufacturer, rationale, entity_key, ecfg
|
||||||
))
|
))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
@@ -1,28 +1,33 @@
|
|||||||
from homeassistant.components.water_heater import WaterHeaterEntity, WaterHeaterEntityFeature
|
from homeassistant.components.water_heater import WaterHeaterEntity, WaterHeaterEntityFeature
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
Platform,
|
Platform,
|
||||||
CONF_DEVICE_ID,
|
|
||||||
CONF_DEVICE,
|
|
||||||
CONF_ENTITIES,
|
|
||||||
ATTR_TEMPERATURE
|
ATTR_TEMPERATURE
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
DOMAIN,
|
|
||||||
DEVICES
|
|
||||||
)
|
|
||||||
from .midea_entities import MideaEntity
|
from .midea_entities import MideaEntity
|
||||||
|
from . import load_device_config
|
||||||
|
|
||||||
|
|
||||||
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)
|
account_bucket = hass.data.get(DOMAIN, {}).get("accounts", {}).get(config_entry.entry_id)
|
||||||
device = hass.data[DOMAIN][DEVICES][device_id].get(CONF_DEVICE)
|
if not account_bucket:
|
||||||
manufacturer = hass.data[DOMAIN][DEVICES][device_id].get("manufacturer")
|
async_add_entities([])
|
||||||
rationale = hass.data[DOMAIN][DEVICES][device_id].get("rationale")
|
return
|
||||||
entities = hass.data[DOMAIN][DEVICES][device_id].get(CONF_ENTITIES).get(Platform.WATER_HEATER)
|
device_list = account_bucket.get("device_list", {})
|
||||||
|
coordinator_map = account_bucket.get("coordinator_map", {})
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
if entities is not None:
|
for device_id, info in device_list.items():
|
||||||
for entity_key, config in entities.items():
|
device_type = info.get("type")
|
||||||
devs.append(MideaWaterHeaterEntityEntity(device, manufacturer, rationale, entity_key, config))
|
sn8 = info.get("sn8")
|
||||||
|
config = load_device_config(hass, device_type, sn8) or {}
|
||||||
|
entities_cfg = (config.get("entities") or {}).get(Platform.WATER_HEATER, {})
|
||||||
|
manufacturer = config.get("manufacturer")
|
||||||
|
rationale = config.get("rationale")
|
||||||
|
coordinator = coordinator_map.get(device_id)
|
||||||
|
device = coordinator.device if coordinator else None
|
||||||
|
for entity_key, ecfg in entities_cfg.items():
|
||||||
|
devs.append(MideaWaterHeaterEntityEntity(device, manufacturer, rationale, entity_key, ecfg))
|
||||||
async_add_entities(devs)
|
async_add_entities(devs)
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user