diff --git a/custom_components/haier_evo/__init__.py b/custom_components/haier_evo/__init__.py index 19278e6..025c781 100644 --- a/custom_components/haier_evo/__init__.py +++ b/custom_components/haier_evo/__init__.py @@ -2,14 +2,23 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.loader import async_get_integration import asyncio from . import api from .const import DOMAIN import logging + + _LOGGER = logging.getLogger(__name__) + PLATFORMS: list[str] = ["climate"] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + integration = await async_get_integration(hass, DOMAIN) + _LOGGER.debug(f'Integration version: {integration.version}') + haier_object = api.Haier(hass, entry.data["email"], entry.data["password"]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = haier_object await hass.async_add_executor_job( diff --git a/custom_components/haier_evo/api.py b/custom_components/haier_evo/api.py index 3340852..73cb3fc 100644 --- a/custom_components/haier_evo/api.py +++ b/custom_components/haier_evo/api.py @@ -11,12 +11,14 @@ from homeassistant.core import HomeAssistant from homeassistant import config_entries, exceptions from urllib.parse import urlparse, parse_qs +from . import yaml_helper import websocket -from websocket._exceptions import WebSocketConnectionClosedException +from websocket._exceptions import WebSocketConnectionClosedException, WebSocketException from enum import Enum + SST_CLOUD_API_URL = "https://api.sst-cloud.com/" API_PATH = "https://evo.haieronline.ru" API_LOGIN = "v1/users/auth/sign-in" @@ -115,21 +117,24 @@ def pull_data(self): and "application/json" in resp.headers.get("content-type") and resp.json().get("data", {}).get("presentation", {}).get("layout", {}).get('scrollContainer', []) ): + _LOGGER.debug(resp.text) containers = resp.json().get("data", {}).get("presentation", {}).get("layout", {}).get('scrollContainer', []) for item in containers: if item.get("contractName", "") == "deviceList": state_data = item.get("state", {}) state_json = json.loads(state_data) - # haierevo://device?deviceId=12:34:56:78:90:68&type=AC&serialNum=AAC0M1E0000000000000&uitype=AC_BASE - device_title = state_json.get('items', [{}])[0].get('title', '') # only one device is supported - device_link = state_json.get('items', [{}])[0].get('action', {}).get('link', '') - parsed_link = urlparse(device_link) - query_params = parse_qs(parsed_link.query) - device_mac = query_params.get('deviceId', [''])[0] - device_mac = device_mac.replace('%3A', ':') - device_serial = query_params.get('serialNum', [''])[0] - _LOGGER.debug(f"Received device successfully, device title {device_title}, device mac {device_mac}, device serial {device_serial}") - self.devices.append(HaierAC(device_mac, device_serial, device_title, self)) + devices = state_json.get('items', [{}]) + for d in devices: + # haierevo://device?deviceId=12:34:56:78:90:68&type=AC&serialNum=AAC0M1E0000000000000&uitype=AC_BASE + device_title = d.get('title', '') # only one device is supported + device_link = d.get('action', {}).get('link', '') + parsed_link = urlparse(device_link) + query_params = parse_qs(parsed_link.query) + device_mac = query_params.get('deviceId', [''])[0] + device_mac = device_mac.replace('%3A', ':') + device_serial = query_params.get('serialNum', [''])[0] + _LOGGER.debug(f"Received device successfully, device title {device_title}, device mac {device_mac}, device serial {device_serial}") + self.devices.append(HaierAC(device_mac, device_serial, device_title, self)) break else: @@ -155,6 +160,9 @@ class SocketStatus(Enum): NOT_INITIALIZED = 3 + + + class HaierAC: def __init__(self, device_mac: str, device_serial: str, device_title: str, haier: Haier): self._haier = haier @@ -162,9 +170,10 @@ def __init__(self, device_mac: str, device_serial: str, device_title: str, haier self._hass:HomeAssistant = haier.hass self._id = device_mac - self.model_name = "AC" self._device_name = device_title + # the following values are updated below + self.model_name = "AC" self._current_temperature = 0 self._target_temperature = 0 self._status = None @@ -173,11 +182,20 @@ def __init__(self, device_mac: str, device_serial: str, device_title: str, haier self._min_temperature = 7 self._max_temperature = 35 self._sw_version = None + # config values, updated below + self._config = None + self._config_current_temperature = None + self._config_mode = None + self._config_fan_mode = None + self._config_status = None + self._config_target_temperature = None + self._config_command_name = None + self._disconnect_requested = False status_url = API_STATUS.replace("{mac}", self._id) - _LOGGER.debug(f"Getting initial status of device {self._id}, url: {status_url}") + _LOGGER.info(f"Getting initial status of device {self._id}, url: {status_url}") resp = requests.get( status_url, headers={"X-Auth-token": self._haier._token} @@ -187,17 +205,33 @@ def __init__(self, device_mac: str, device_serial: str, device_title: str, haier and resp.json().get("attributes", {}) ): _LOGGER.debug(f"Update device {self._id} status code: {resp.status_code}") + _LOGGER.debug(resp.text) + device_info = resp.json().get("info", {}) + device_model = device_info.get("model", "AC") + _LOGGER.debug(f"Device model {device_model}") + self.model_name = device_model + self._config = yaml_helper.DeviceConfig(device_model) + + # read config values + self._config_current_temperature = self._config.get_id_by_name('current_temperature') + self._config_mode = self._config.get_id_by_name('mode') + self._config_fan_mode = self._config.get_id_by_name('fan_mode') + self._config_status = self._config.get_id_by_name('status') + self._config_target_temperature = self._config.get_id_by_name('target_temperature') + self._config_command_name = self._config.get_command_name() + _LOGGER.debug(f"The following values are used: current temp - {self._config_current_temperature}, mode - {self._config_mode}, fan speed - {self._config_fan_mode}, status - {self._config_status}, target temp - {self._config_target_temperature}") + attributes = resp.json().get("attributes", {}) for attr in attributes: - if attr.get('name', '') == "0": # Температура в комнате + if attr.get('name', '') == self._config_current_temperature: # Температура в комнате self._current_temperature = int(attr.get('currentValue')) - if attr.get('name', '') == "5": # Режимы (0 - Авто, 1 - Охлаждение, 4 - Нагрев, 6 - Вентилятор, 2 - Осушение) + elif attr.get('name', '') == self._config_mode: # Режимы self._mode = int(attr.get('currentValue')) - if attr.get('name', '') == "6": # Скорость вентилятора (0 - Авто, 1 - Охлаждение, 4 - Нагрев, 6 - Вентилятор, 2 - Осушение) - self._mode = int(attr.get('currentValue')) - if attr.get('name', '') == "21": # Включение/выключение + elif attr.get('name', '') == self._config_fan_mode: # Скорость вентилятора + self._fan_mode = int(attr.get('currentValue')) + elif attr.get('name', '') == self._config_status: # Включение/выключение self._status = int(attr.get('currentValue')) - if attr.get('name', '') == "31": # Целевая температура + elif attr.get('name', '') == self._config_target_temperature: # Целевая температура self._target_temperature = int(attr.get('currentValue')) self._min_temperature = int(attr.get('range', {}).get('data', {}).get('minValue', 0)) self._max_temperature = int(attr.get('range', {}).get('data', {}).get('maxValue', 0)) @@ -248,8 +282,10 @@ def _on_message(self, ws: websocket.WebSocket, message: str) -> None: self._handle_status_update(message_dict) elif message_type == "command_response": pass + elif message_type == "info": + pass else: - _LOGGER.error(f"Got unknown message of type: {message_type}") + _LOGGER.debug(f"Got unknown message of type: {message_type}") @@ -274,19 +310,18 @@ def _handle_status_update(self, received_message: dict) -> None: _LOGGER.debug(f"Received status update, message_id {message_id}") for key, value in message_statuses[0]['properties'].items(): - if key == "0": # Температура в комнате + if key == self._config_current_temperature: # Температура в комнате self._current_temperature = int(value) - if key == "5": # Режимы (0 - Авто, 1 - Охлаждение, 4 - Нагрев, 6 - Вентилятор, 2 - Осушение) - self._mode = int(value) - if key == "6": # Скорость вентилятора (0 - Авто, 1 - Охлаждение, 4 - Нагрев, 6 - Вентилятор, 2 - Осушение) - self._fan_mode = int(value) - if key == "21": # Включение/выключение + if key == self._config_mode: # Режимы + self._mode = self._config.get_value_from_mappings(self._config_mode, int(value)) + if key == self._config_fan_mode: # Скорость вентилятора + self._fan_mode = self._config.get_value_from_mappings(self._config_fan_mode, int(value)) + if key == self._config_status: # Включение/выключение self._status = int(value) - if key == "31": # Целевая температура + if key == self._config_target_temperature: # Целевая температура self._target_temperature = int(value) - def _on_open(self, ws: websocket.WebSocket) -> None: _LOGGER.debug("Websocket opened") @@ -319,7 +354,11 @@ def connect(self) -> None: ]: self._socket_status = SocketStatus.INITIALIZING _LOGGER.info(f"Connecting to websocket ({API_WS_PATH})") - self._socket_app.run_forever() + try: + self._socket_app.run_forever() + except WebSocketException: # websocket._exceptions.WebSocketException: socket is already opened + pass + else: _LOGGER.info( f"Can not attempt socket connection because of current " @@ -385,7 +424,6 @@ def get_fan_mode(self) -> str: def get_status(self) -> str: return self._status - def setTemperature(self, temp) -> None: self._target_temperature = temp @@ -393,29 +431,31 @@ def setTemperature(self, temp) -> None: { "action": "operation", "macAddress": self._id, - "commandName": "3", + "commandName": self._config_command_name, "commands": [ { - "commandName": "31", + "commandName": self._config_target_temperature, "value": str(temp) } ] })) - def switchOn(self, hvac_mode=0) -> None: # default hvac mode is 0 - Авто + def switchOn(self, hvac_mode="auto") -> None: + hvac_mode_haier = self._config.get_haier_code_from_mappings(self._config_mode, hvac_mode) + self._send_message(json.dumps( { "action": "operation", "macAddress": self._id, - "commandName": "3", + "commandName": self._config_command_name, "commands": [ { - "commandName": "21", + "commandName": self._config_status, "value": "1" }, { - "commandName": "5", - "value": str(hvac_mode) + "commandName": self._config_mode, + "value": str(hvac_mode_haier) } ] })) @@ -427,10 +467,10 @@ def switchOff(self) -> None: { "action": "operation", "macAddress": self._id, - "commandName": "3", + "commandName": self._config_command_name, "commands": [ { - "commandName": "21", + "commandName": self._config_status, "value": "0" } ] @@ -438,17 +478,19 @@ def switchOff(self) -> None: self._status = 0 def setFanMode(self, fan_mode) -> None: + fan_mode_haier = self._config.get_haier_code_from_mappings(self._config_fan_mode, fan_mode) + self._fan_mode = fan_mode self._send_message(json.dumps( { "action": "operation", "macAddress": self._id, - "commandName": "3", + "commandName": self._config_command_name, "commands": [ { - "commandName": "6", - "value": str(fan_mode) + "commandName": self._config_fan_mode, + "value": str(fan_mode_haier) } ] })) \ No newline at end of file diff --git a/custom_components/haier_evo/climate.py b/custom_components/haier_evo/climate.py index 1c29d51..684024c 100644 --- a/custom_components/haier_evo/climate.py +++ b/custom_components/haier_evo/climate.py @@ -72,52 +72,26 @@ def turn_off(self): @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - if self._module.get_status == 1: - # (0 - Авто, 1 - Охлаждение, 4 - Нагрев, 6 - Вентилятор, 2 - Осушение) - if self._module.get_mode == 0: - return HVACMode.AUTO - if self._module.get_mode == 1: - return HVACMode.COOL - if self._module.get_mode == 2: - return HVACMode.DRY - if self._module.get_mode == 4: - return HVACMode.HEAT - if self._module.get_mode == 6: - return HVACMode.FAN_ONLY + return self._module.get_mode return HVACMode.OFF def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - _LOGGER.warning(f"set_hvac_mode {hvac_mode}") + _LOGGER.debug(f"Setting HVAC mode to {hvac_mode}") if hvac_mode == HVACMode.OFF: self._module.switchOff() else: - if hvac_mode == HVACMode.AUTO: - hvac_mode_int = 0 - elif hvac_mode == HVACMode.COOL: - hvac_mode_int = 1 - elif hvac_mode == HVACMode.DRY: - hvac_mode_int = 2 - elif hvac_mode == HVACMode.HEAT: - hvac_mode_int = 4 - elif hvac_mode == HVACMode.FAN_ONLY: - hvac_mode_int = 6 - - self._module.switchOn(hvac_mode_int) + self._module.switchOn(hvac_mode) def set_fan_mode(self, fan_mode): - # FAN_AUTO - 5, FAN_LOW - 3, FAN_MEDIUM - 2, FAN_HIGH - 1 - if fan_mode == FAN_HIGH: - fan_mode_int = 1 - if fan_mode == FAN_MEDIUM: - fan_mode_int = 2 - if fan_mode == FAN_LOW: - fan_mode_int = 3 - if fan_mode == FAN_AUTO: - fan_mode_int = 5 + """Set new target fan mode.""" + _LOGGER.debug(f"Setting fan mode to {fan_mode}") + self._module.setFanMode(fan_mode) - self._module.setFanMode(fan_mode_int) + @property + def fan_mode(self) -> str: + return self._module.get_fan_mode def update(self) -> None: @@ -144,17 +118,7 @@ def current_temperature(self) -> float: def target_temperature(self) -> float: return self._module.get_target_temperature - @property - def fan_mode(self) -> str: - # FAN_AUTO - 5, FAN_LOW - 3, FAN_MEDIUM - 2, FAN_HIGH - 1 - if self._module.get_fan_mode == 1: - return FAN_HIGH - if self._module.get_fan_mode == 2: - return FAN_MEDIUM - if self._module.get_fan_mode == 3: - return FAN_LOW - if self._module.get_fan_mode == 5: - return FAN_AUTO + @property def device_info(self): diff --git a/custom_components/haier_evo/devices/AS20HPL1HRA.yaml b/custom_components/haier_evo/devices/AS20HPL1HRA.yaml new file mode 100644 index 0000000..0cb28c7 --- /dev/null +++ b/custom_components/haier_evo/devices/AS20HPL1HRA.yaml @@ -0,0 +1,32 @@ +command_name: "3" +attributes: + - name: current_temperature + id: "0" + - name: mode + id: "5" + mappings: + - haier: 0 + value: auto + - haier: 1 + value: cool + - haier: 2 + value: dry + - haier: 4 + value: heat + - haier: 6 + value: fan_only + - name: fan_mode + id: "6" + mappings: + - haier: 1 + value: high + - haier: 2 + value: medium + - haier: 3 + value: low + - haier: 5 + value: auto + - name: status + id: "21" + - name: target_temperature + id: "31" \ No newline at end of file diff --git a/custom_components/haier_evo/devices/AS20HPL2HRA.yaml b/custom_components/haier_evo/devices/AS20HPL2HRA.yaml new file mode 100644 index 0000000..c943c96 --- /dev/null +++ b/custom_components/haier_evo/devices/AS20HPL2HRA.yaml @@ -0,0 +1,32 @@ +command_name: "4" +attributes: + - name: current_temperature + id: "36" + - name: mode + id: "2" + mappings: + - haier: 0 + value: auto + - haier: 1 + value: cool + - haier: 2 + value: dry + - haier: 4 + value: heat + - haier: 6 + value: fan_only + - name: fan_mode + id: "4" + mappings: + - haier: 1 + value: high + - haier: 2 + value: medium + - haier: 3 + value: low + - haier: 5 + value: auto + - name: status + id: "21" + - name: target_temperature + id: "0" \ No newline at end of file diff --git a/custom_components/haier_evo/devices/__init__.py b/custom_components/haier_evo/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/haier_evo/devices/__pycache__/__init__.cpython-312.pyc b/custom_components/haier_evo/devices/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5719203 Binary files /dev/null and b/custom_components/haier_evo/devices/__pycache__/__init__.cpython-312.pyc differ diff --git a/custom_components/haier_evo/devices/default.yaml b/custom_components/haier_evo/devices/default.yaml new file mode 100644 index 0000000..0cb28c7 --- /dev/null +++ b/custom_components/haier_evo/devices/default.yaml @@ -0,0 +1,32 @@ +command_name: "3" +attributes: + - name: current_temperature + id: "0" + - name: mode + id: "5" + mappings: + - haier: 0 + value: auto + - haier: 1 + value: cool + - haier: 2 + value: dry + - haier: 4 + value: heat + - haier: 6 + value: fan_only + - name: fan_mode + id: "6" + mappings: + - haier: 1 + value: high + - haier: 2 + value: medium + - haier: 3 + value: low + - haier: 5 + value: auto + - name: status + id: "21" + - name: target_temperature + id: "31" \ No newline at end of file diff --git a/custom_components/haier_evo/manifest.json b/custom_components/haier_evo/manifest.json index 7e2cf42..5f7fb77 100644 --- a/custom_components/haier_evo/manifest.json +++ b/custom_components/haier_evo/manifest.json @@ -1,6 +1,6 @@ { "domain": "haier_evo", - "name": "Haier Evo Air Conditioner", + "name": "Haier Evo", "codeowners": ["@and7ey"], "config_flow": true, "dependencies": [], @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/and7ey/haier_evo/issues", "requirements": ["requests"], - "version": "0.1.0", + "version": "0.2.1", "zeroconf": [] } \ No newline at end of file diff --git a/custom_components/haier_evo/yaml_helper.py b/custom_components/haier_evo/yaml_helper.py new file mode 100644 index 0000000..2d585de --- /dev/null +++ b/custom_components/haier_evo/yaml_helper.py @@ -0,0 +1,66 @@ +""" +Config parser for Haier Evo devices. +""" + +import logging + +from homeassistant.util.yaml import load_yaml + +import custom_components.haier_evo.devices as config_dir +from os.path import dirname, exists, join, splitext + +_LOGGER = logging.getLogger(__name__) + + +class DeviceConfig: + """Representation of a device config.""" + + def __init__(self, fname): + """Initialize the device config. + Args: + fname (string): The filename of the yaml config to load.""" + _CONFIG_DIR = dirname(config_dir.__file__) + self._fname = fname + filename = join(_CONFIG_DIR, fname) + '.yaml' + if not exists(filename): + filename = join(_CONFIG_DIR, 'default') + '.yaml' + self._config = load_yaml(filename) + _LOGGER.debug("Loaded device config %s", fname) + + def get_command_name(self): + return self._config['command_name'] + + + def get_name_by_id(self, id): + attributes = self._config['attributes'] + for attr in attributes: + if attr.get('id') == id: + return attr.get('name') + return None + + def get_id_by_name(self, name): + attributes = self._config['attributes'] + for attr in attributes: + if attr.get('name') == name: + return attr.get('id') + return None + + def get_value_from_mappings(self, id, haier_value): + attributes = self._config['attributes'] + for attr in attributes: + if attr.get('id') == id: + mappings = attr.get('mappings') + for mapping in mappings: + if mapping.get('haier') == haier_value: + return mapping.get('value') + return None + + def get_haier_code_from_mappings(self, id, value): + attributes = self._config['attributes'] + for attr in attributes: + if attr.get('id') == id: + mappings = attr.get('mappings') + for mapping in mappings: + if mapping.get('value') == value: + return mapping.get('haier') + return None \ No newline at end of file