├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── .travis └── push.sh ├── LICENSE ├── README.md ├── custom_components └── midea_ac │ ├── __init__.py │ ├── climate.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ └── translations │ └── en.json ├── hacs.json ├── info.md └── pcap-decrypt.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://paypal.me/andersonshatch', 'https://monzo.me/joshanderson'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (错误报告) 3 | about: Something is not working right (运行不正常) 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug (描述一下问题)** 11 | A clear and concise description of what the bug is. 12 | 13 | **Screenshots / Logs / Pcap File(屏幕截图/日志/抓包文件)** 14 | If applicable, add screenshots or your home-assistant log file or pcap file to help explain your problem. 15 | 请提交相关截图,日志,抓包让开发者能够更快的解决问题 16 | 17 | **Versions (版本信息)** 18 | - Home Assistant version: 19 | - Midea msmart version: 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request (功能需求) 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like (描述需求)** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Screenshots / Logs / Pcap File(屏幕截图/日志/抓包文件)** 14 | If applicable, add screenshots or your home-assistant log file or pcap file to help explain your problem. 15 | 请提交相关截图,日志,抓包让开发者能够更快的解决问题 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .vscode-upload.json 106 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | install: 5 | - pip install -r requirements.txt 6 | script: 7 | - python setup.py sdist bdist_wheel 8 | deploy: 9 | provider: pypi 10 | user: NeoAcheron 11 | skip_cleanup: true 12 | password: 13 | secure: Znp6wiYWcfjmIWl3Gsq+dqGLLNp3aUVDu2QTVqX5gJ9zc1MVPZE5mpw0Wfl9AUdCmO/xj7ZZ7lbhKh66xYbuvKNwStJUrSKdUL5JsNFGRxSsfaM++1wtiwNBzsATaVjBLsXhUomWfet78qsqvwLlBAT0prthoT3FP/5N8tP0TMauh2pdc+DoFyYe/D1eiTffZjiYj44Dycb965Ys5Voqu55hSE8Tiy1vKwT6wxiBvDjBSeNafpcyoLK0kDLEQomX57u7Z4T4OWPvcmIpS/U60iJ7WT4U+HxjwgvGsROPZ5vlD9sh+SZk+6GMb5nxo8jlp9RZz6C840TR7PGLskO+MvoFQuSdLEBcnnZOu9SxdYKO04LfqKBskM0n8TCDuGhLSTupS7ReaGOn5CeJmmE5qBpUm+K8Juz38zh7x0bF7rzmonUuNvSN/nY5lyg7yl457P2eeQCN/fhDEhetgp7jC6BiNJblq28DpUKRuRGgu1XOrTGk8Mdw8KXwO2Z3tE7Xi4Xz1vLv8zYs1NURJeLR3bDqfeyK5ALQbPxLhq+wIyGWzw4wuziNcaneJN6A3EUzBAu2I+jcKX/b/eW1PdBPtAxWs3r06axpCIa3Je1cnpLskqJsUar+Jw2zI82i1pM813z3/LoXdeHc10z7sJ4tLpWn49rIf/LnetYs23D3qJc= 14 | -------------------------------------------------------------------------------- /.travis/push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo Currently on branch `git branch` 4 | 5 | bumpversion patch --no-tag --allow-dirty --no-commit --list > .temp 6 | CURRENT_VERSION=`cat .temp | grep current_version | sed -r s,"^.*=",,` 7 | NEW_VERSION=`cat .temp | grep new_version | sed -r s,"^.*=",,` 8 | 9 | git add .bumpversion.cfg 10 | git add custom_components.json 11 | git add midea.py 12 | git add setup.py 13 | git add midea/*.py 14 | 15 | git commit -m "Version Changed from ${CURRENT_VERSION} -> ${NEW_VERSION}" 16 | git tag "v${NEW_VERSION}" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 NeoAcheron 4 | Copyright (c) 2019 Josh Anderson (@andersonshatch) 5 | Copyright (c) 2020 Mac Zhou (@mac-zhou) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Home Assistant Custom Integration for Midea Group(Hualing, Senville, Klimaire, AirCon, Century, Pridiom, Thermocore, Comfee, Alpine Home Air, Artel, Beko, Electrolux, Galactic, Idea, Inventor, Kaisai, Mitsui, Mr. Cool, Neoclima, Olimpia Splendid, Pioneer, QLIMA, Rotenso, Royal Clima, Qzen, Toshiba, Carrier, Goodman, Friedrich, Samsung, Kenmore, Trane, Lennox, LG and much more) Air Conditioners via LAN. 2 | 3 | Tested with Home Assistant 2021.7.2. 4 | 5 | ## Attention!!! 6 | Version >= 0.1.27, The ENTITY ID of the climate devices has been changed. if you have any problem with an old entity being "unavailable", you should check whats the new ID name of the entity and change it in lovelace. 7 | 8 | ## Installation 9 | 10 | ### Install from HACS 11 | [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/mac-zhou/midea-ac-py) [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 12 | 13 | Search the HACS Store for ```midea_ac``` 14 | 15 | ### Install manually 16 | 1. Clone this repo 17 | 2. Place the `custom_components/midea_ac` folder into your `custom_components` folder 18 | 19 | ## Configuration 20 | 21 | **Configuration variables:** 22 | 23 | key | description | example 24 | :--- | :--- | :--- 25 | **platform (Required)** | The platform name. | midea_ac 26 | **host (Required)** | Midea AC Device's IP Address. | 192.168.1.100 27 | **id (Required)** | Midea AC Device's applianceId. | 123456789012345 28 | **token (Optional)** | Midea AC Device's token, V3 is required. | ACEDDA53831AE5DC...(Length 128) 29 | **k1 (Optional)** | Midea AC Device's k1, V3 is required. | CFFA10FC...(Length 64) 30 | **temp_step (Optional)** | Step size for temperature set point, default is 1.0 | 0.5 31 | **prompt_tone (Optional)** | Prompt Tone, default is true. | false 32 | **keep_last_known_online_state (Optional)** | Set this to true if you see too many `unavailable` in log. | true 33 | **use_fan_only_workaround (Optional)** | Set this to true if you need to turn off device updates because they turn device on and to fan_only | true 34 | 35 | **Example configuration.yaml:** 36 | ```yaml 37 | climate: 38 | - platform: midea_ac 39 | host: 192.168.1.100 40 | id: 123456789012345 41 | # v3 need token and k1(key) 42 | # token: ACEDDA53831AE5DC...(Length 128) 43 | # k1: CFFA10FC...(Length 64) 44 | ``` 45 | 46 | ## How to Get Configuration variables: 47 | - `midea-discover` can help you discover Midea devices from the LAN. 48 | ```zsh 49 | pip3 install msmart 50 | midea-discover 51 | ``` 52 | - Basic Usage 53 | ``` 54 | Usage: midea-discover [OPTIONS] 55 | 56 | Discover Midea Deivces and Get Device's info 57 | 58 | Options: 59 | -d, --debug Enable debug logging 60 | -c, --amount INTEGER Number of broadcast packets, default is 1. 61 | if you have many devices, you may change this value. 62 | -a, --account TEXT Your email address for your Midea account. 63 | -p, --password TEXT Your password for your Midea account. 64 | -i, --ip TEXT IP address of Midea device. you can use: 65 | - broadcasts don't work. 66 | - just get one device's info. 67 | - an error occurred. 68 | --help Show this message and exit. 69 | ``` 70 | ***Note***: 71 | - This component only supports devices with model 0xac (air conditioner) and words `supported` in the output. 72 | - Configure v3 devices need `token` and `k1`. 73 | - You `midea-discover` when broadcasts don't work. 74 | - `midea-discover` use a registered account of `MSmartHome` [[AppStore]](https://apps.apple.com/sg/app/midea-home/id1254346490) [[GooglePlay]](https://play.google.com/store/apps/details?id=com.midea.ai.overseas) to get Token and K1(key). 75 | it's my account, but now it’s an open account. 76 | If you just only get Token and K1(key) with this account, I and others can't get your information through this account. 77 | Don't use this account to login to the APP and add device, this may reveal your information. Of course, you can use your own account,this is also the way I recommend. 78 | ```zsh 79 | midea-discover -a YOUR_ACCOUNT -p YOUR_PASSWORD 80 | ``` 81 | 82 | ## Buy me a cup of coffee 83 | 84 | - [via Paypal](https://www.paypal.me/himaczhou) 85 | - [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) 86 | - [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) 87 | - [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) 88 | 89 | Your donation will make me work better for this project. 90 | -------------------------------------------------------------------------------- /custom_components/midea_ac/__init__.py: -------------------------------------------------------------------------------- 1 | """Integration for Midea Smart AC.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN 6 | from homeassistant.core import HomeAssistant 7 | from msmart.device import air_conditioning as ac 8 | 9 | # Local constants 10 | from .const import ( 11 | DOMAIN, 12 | CONF_K1 13 | ) 14 | 15 | 16 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 17 | """Set up a Midea AC device from a config entry.""" 18 | 19 | # Ensure the global data dict exists 20 | hass.data.setdefault(DOMAIN, {}) 21 | 22 | # Get config data from entry 23 | config = config_entry.data 24 | 25 | # Attempt to get device from global data 26 | id = config.get(CONF_ID) 27 | device = hass.data[DOMAIN].get(id) 28 | 29 | # Construct a new device if necessary 30 | if device is None: 31 | # Construct the device 32 | id = config.get(CONF_ID) 33 | host = config.get(CONF_HOST) 34 | port = config.get(CONF_PORT) 35 | device = ac(host, int(id), port) 36 | 37 | # Configure token and k1 as needed 38 | token = config.get(CONF_TOKEN) 39 | k1 = config.get(CONF_K1) 40 | if token and k1: 41 | await hass.async_add_executor_job(device.authenticate, k1, token) 42 | 43 | hass.data[DOMAIN][id] = device 44 | 45 | # Create platform entries 46 | hass.async_create_task( 47 | hass.config_entries.async_forward_entry_setup(config_entry, "climate")) 48 | hass.async_create_task( 49 | hass.config_entries.async_forward_entry_setup(config_entry, "sensor")) 50 | 51 | # Reload entry when its updated 52 | config_entry.async_on_unload( 53 | config_entry.add_update_listener(async_reload_entry)) 54 | 55 | return True 56 | 57 | 58 | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 59 | 60 | # Get config data from entry 61 | config = config_entry.data 62 | 63 | # Remove device from global data 64 | id = config.get(CONF_ID) 65 | hass.data[DOMAIN].pop(id) 66 | 67 | await hass.config_entries.async_forward_entry_unload(config_entry, "climate") 68 | await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") 69 | 70 | return True 71 | 72 | 73 | async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: 74 | await hass.config_entries.async_reload(config_entry.entry_id) 75 | -------------------------------------------------------------------------------- /custom_components/midea_ac/climate.py: -------------------------------------------------------------------------------- 1 | """ 2 | A climate platform that adds support for Midea air conditioning units. 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://github.com/mac-zhou/midea-ac-py 6 | 7 | This is still early work in progress 8 | """ 9 | from __future__ import annotations 10 | 11 | import datetime 12 | import logging 13 | 14 | from homeassistant.config_entries import ConfigEntry 15 | from homeassistant.const import TEMP_CELSIUS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_ID 16 | try: 17 | from homeassistant.components.climate import ClimateEntity 18 | except ImportError: 19 | from homeassistant.components.climate import ClimateDevice as ClimateEntity 20 | from homeassistant.components.climate.const import ( 21 | SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, 22 | SUPPORT_PRESET_MODE, PRESET_NONE, PRESET_ECO, PRESET_BOOST) 23 | from homeassistant.core import HomeAssistant 24 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 25 | from msmart.device import air_conditioning as ac 26 | 27 | # Local constants 28 | from .const import ( 29 | DOMAIN, 30 | CONF_PROMPT_TONE, 31 | CONF_TEMP_STEP, 32 | CONF_INCLUDE_OFF_AS_STATE, 33 | CONF_USE_FAN_ONLY_WORKAROUND, 34 | CONF_KEEP_LAST_KNOWN_ONLINE_STATE 35 | ) 36 | 37 | _LOGGER = logging.getLogger(__name__) 38 | 39 | # Override default scan interval? 40 | SCAN_INTERVAL = datetime.timedelta(seconds=15) 41 | 42 | 43 | async def async_setup_entry( 44 | hass: HomeAssistant, 45 | config_entry: ConfigEntry, 46 | add_entities: AddEntitiesCallback, 47 | ) -> None: 48 | """Setup the climate platform for Midea Smart AC.""" 49 | 50 | _LOGGER.info("Setting up climate platform.") 51 | 52 | # Get config and options data from entry 53 | config = config_entry.data 54 | options = config_entry.options 55 | 56 | # Fetch device from global data 57 | id = config.get(CONF_ID) 58 | device = hass.data[DOMAIN][id] 59 | 60 | # Query device capabilities 61 | _LOGGER.info("Querying device capabilities.") 62 | await hass.async_add_executor_job(device.get_capabilities) 63 | 64 | add_entities([ 65 | MideaClimateACDevice(hass, device, options) 66 | ]) 67 | 68 | 69 | class MideaClimateACDevice(ClimateEntity): 70 | """Representation of a Midea climate AC device.""" 71 | 72 | def __init__(self, hass, device, options: dict): 73 | """Initialize the climate device.""" 74 | 75 | self.hass = hass 76 | self._device = device 77 | 78 | # Apply options 79 | self._device.prompt_tone = options.get(CONF_PROMPT_TONE) 80 | self._device.keep_last_known_online_state = options.get( 81 | CONF_KEEP_LAST_KNOWN_ONLINE_STATE) 82 | 83 | # Display on the AC should use the same unit as homeassistant 84 | self._device.fahrenheit = ( 85 | hass.config.units.temperature_unit == TEMP_FAHRENHEIT) 86 | 87 | self._target_temperature_step = options.get(CONF_TEMP_STEP) 88 | self._include_off_as_state = options.get(CONF_INCLUDE_OFF_AS_STATE) 89 | self._use_fan_only_workaround = options.get( 90 | CONF_USE_FAN_ONLY_WORKAROUND) 91 | 92 | self._operation_list = device.supported_operation_modes 93 | if self._include_off_as_state: 94 | self._operation_list.append("off") 95 | 96 | self._fan_list = ac.fan_speed_enum.list() 97 | self._swing_list = device.supported_swing_modes 98 | 99 | self._changed = False 100 | 101 | async def apply_changes(self) -> None: 102 | if not self._changed: 103 | return 104 | await self.hass.async_add_executor_job(self._device.apply) 105 | await self.async_update_ha_state() 106 | self._changed = False 107 | 108 | async def async_update(self) -> None: 109 | """Retrieve latest state from the appliance if no changes made, 110 | otherwise update the remote device state.""" 111 | if self._changed: 112 | await self.hass.async_add_executor_job(self._device.apply) 113 | self._changed = False 114 | elif not self._use_fan_only_workaround: 115 | await self.hass.async_add_executor_job(self._device.refresh) 116 | 117 | async def async_added_to_hass(self) -> None: 118 | """Run when entity about to be added.""" 119 | await super().async_added_to_hass() 120 | 121 | # Populate data ASAP 122 | await self.async_update() 123 | 124 | @property 125 | def device_info(self) -> dict: 126 | return { 127 | "identifiers": { 128 | (DOMAIN, self._device.id) 129 | }, 130 | "name": self.name, 131 | "manufacturer": "Midea", 132 | } 133 | 134 | @property 135 | def available(self) -> bool: 136 | """Checks if the appliance is available for commands.""" 137 | return self._device.online 138 | 139 | @property 140 | def supported_features(self) -> int: 141 | """Return the list of supported features.""" 142 | return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE | SUPPORT_PRESET_MODE 143 | 144 | @property 145 | def target_temperature_step(self) -> float: 146 | """Return the supported step of target temperature.""" 147 | return self._target_temperature_step 148 | 149 | @property 150 | def hvac_modes(self) -> list: 151 | """Return the list of available operation modes.""" 152 | return self._operation_list 153 | 154 | @property 155 | def fan_modes(self) -> list: 156 | """Return the list of available fan modes.""" 157 | return self._fan_list 158 | 159 | @property 160 | def swing_modes(self) -> list: 161 | """List of available swing modes.""" 162 | return self._swing_list 163 | 164 | @property 165 | def assumed_state(self) -> bool: 166 | """Assume state rather than refresh to workaround fan_only bug.""" 167 | return self._use_fan_only_workaround 168 | 169 | @property 170 | def should_poll(self) -> bool: 171 | """Poll the appliance for changes, there is no notification capability in the Midea API""" 172 | return not self._use_fan_only_workaround 173 | 174 | @property 175 | def unique_id(self) -> str: 176 | return f"{self._device.id}" 177 | 178 | @property 179 | def name(self) -> str: 180 | """Return the name of the climate device.""" 181 | return f"{DOMAIN}_{self._device.id}" 182 | 183 | @property 184 | def temperature_unit(self) -> str: 185 | """Return the unit of measurement.""" 186 | return TEMP_CELSIUS 187 | 188 | @property 189 | def current_temperature(self) -> float: 190 | """Return the current temperature.""" 191 | return self._device.indoor_temperature 192 | 193 | @property 194 | def target_temperature(self) -> float: 195 | """Return the temperature we try to reach.""" 196 | return self._device.target_temperature 197 | 198 | @property 199 | def hvac_mode(self) -> str: 200 | """Return current operation ie. heat, cool, idle.""" 201 | if self._include_off_as_state and not self._device.power_state: 202 | return "off" 203 | return self._device.operational_mode.name 204 | 205 | @property 206 | def fan_mode(self) -> str: 207 | """Return the fan setting.""" 208 | return self._device.fan_speed.name 209 | 210 | @property 211 | def swing_mode(self) -> str: 212 | """Return the swing setting.""" 213 | return self._device.swing_mode.name 214 | 215 | @property 216 | def is_on(self) -> bool: 217 | """Return true if the device is on.""" 218 | return self._device.power_state 219 | 220 | async def async_set_temperature(self, **kwargs) -> None: 221 | """Set new target temperatures.""" 222 | if kwargs.get(ATTR_TEMPERATURE) is not None: 223 | # grab temperature from front end UI 224 | temp = kwargs.get(ATTR_TEMPERATURE) 225 | 226 | # round temperature to nearest .5 227 | temp = round(temp * 2) / 2 228 | 229 | # send temperature to unit 230 | self._device.target_temperature = temp 231 | self._changed = True 232 | await self.apply_changes() 233 | 234 | async def async_set_swing_mode(self, swing_mode) -> None: 235 | """Set swing mode.""" 236 | self._device.swing_mode = ac.swing_mode_enum[swing_mode] 237 | self._changed = True 238 | await self.apply_changes() 239 | 240 | async def async_set_fan_mode(self, fan_mode) -> None: 241 | """Set fan mode.""" 242 | """Fix key error when calling from HomeKit""" 243 | fan_mode = fan_mode.capitalize() 244 | self._device.fan_speed = ac.fan_speed_enum[fan_mode] 245 | self._changed = True 246 | await self.apply_changes() 247 | 248 | async def async_set_hvac_mode(self, hvac_mode) -> None: 249 | """Set hvac mode.""" 250 | if self._include_off_as_state and hvac_mode == "off": 251 | self._device.power_state = False 252 | else: 253 | if self._include_off_as_state: 254 | self._device.power_state = True 255 | self._device.operational_mode = ac.operational_mode_enum[hvac_mode] 256 | self._changed = True 257 | await self.apply_changes() 258 | 259 | async def async_set_preset_mode(self, preset_mode: str) -> None: 260 | if preset_mode == PRESET_NONE: 261 | self._device.eco_mode = False 262 | self._device.turbo_mode = False 263 | elif preset_mode == PRESET_BOOST: 264 | self._device.eco_mode = False 265 | self._device.turbo_mode = True 266 | elif preset_mode == PRESET_ECO: 267 | self._device.turbo_mode = False 268 | self._device.eco_mode = True 269 | 270 | self._changed = True 271 | await self.apply_changes() 272 | 273 | @property 274 | def preset_modes(self) -> list: 275 | return [PRESET_NONE, PRESET_ECO, PRESET_BOOST] 276 | 277 | @property 278 | def preset_mode(self) -> str: 279 | if self._device.eco_mode: 280 | return PRESET_ECO 281 | elif self._device.turbo_mode: 282 | return PRESET_BOOST 283 | else: 284 | return PRESET_NONE 285 | 286 | async def async_turn_on(self) -> None: 287 | """Turn on.""" 288 | self._device.power_state = True 289 | self._changed = True 290 | await self.apply_changes() 291 | 292 | async def async_turn_off(self) -> None: 293 | """Turn off.""" 294 | self._device.power_state = False 295 | self._changed = True 296 | await self.apply_changes() 297 | 298 | @property 299 | def min_temp(self) -> float: 300 | """Return the minimum temperature.""" 301 | return 17 302 | 303 | @property 304 | def max_temp(self) -> float: 305 | """Return the maximum temperature.""" 306 | return 30 307 | -------------------------------------------------------------------------------- /custom_components/midea_ac/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Midea Smart AC.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow 5 | from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN 6 | from homeassistant.core import callback 7 | from homeassistant.data_entry_flow import FlowResult 8 | import homeassistant.helpers.config_validation as cv 9 | from msmart.device import air_conditioning as ac 10 | import voluptuous as vol 11 | 12 | # Local constants 13 | from .const import ( 14 | DOMAIN, 15 | CONF_K1, 16 | CONF_PROMPT_TONE, 17 | CONF_TEMP_STEP, 18 | CONF_INCLUDE_OFF_AS_STATE, 19 | CONF_USE_FAN_ONLY_WORKAROUND, 20 | CONF_KEEP_LAST_KNOWN_ONLINE_STATE 21 | ) 22 | 23 | 24 | class MideaConfigFlow(ConfigFlow, domain=DOMAIN): 25 | 26 | async def async_step_user(self, user_input) -> FlowResult: 27 | errors = {} 28 | if user_input is not None: 29 | # Set the unique ID and abort if duplicate exists 30 | id = user_input.get(CONF_ID) 31 | await self.async_set_unique_id(id) 32 | self._abort_if_unique_id_configured() 33 | 34 | # Attempt a connection to see if config is valid 35 | device = await self._test_connection(user_input) 36 | 37 | if device: 38 | # Save the device into global data 39 | self.hass.data.setdefault(DOMAIN, {}) 40 | self.hass.data[DOMAIN][id] = device 41 | 42 | # Split user input config data and options 43 | data = { 44 | CONF_ID: id, 45 | CONF_HOST: user_input.get(CONF_HOST), 46 | CONF_PORT: user_input.get(CONF_PORT), 47 | CONF_TOKEN: user_input.get(CONF_TOKEN), 48 | CONF_K1: user_input.get(CONF_K1), 49 | } 50 | options = { 51 | CONF_PROMPT_TONE: user_input.get(CONF_PROMPT_TONE), 52 | CONF_TEMP_STEP: user_input.get(CONF_TEMP_STEP), 53 | CONF_INCLUDE_OFF_AS_STATE: user_input.get(CONF_INCLUDE_OFF_AS_STATE), 54 | CONF_USE_FAN_ONLY_WORKAROUND: user_input.get(CONF_USE_FAN_ONLY_WORKAROUND), 55 | CONF_KEEP_LAST_KNOWN_ONLINE_STATE: user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE), 56 | } 57 | 58 | # Create a config entry with the config data and options 59 | return self.async_create_entry(title=f"{DOMAIN} {id}", data=data, options=options) 60 | else: 61 | # Indicate a connection could not be made 62 | errors["base"] = "cannot_connect" 63 | 64 | 65 | user_input = user_input or {} 66 | 67 | data_schema = vol.Schema({ 68 | vol.Required(CONF_ID, 69 | default=user_input.get(CONF_ID)): cv.string, 70 | vol.Required(CONF_HOST, 71 | default=user_input.get(CONF_HOST)): cv.string, 72 | vol.Optional(CONF_PORT, 73 | default=user_input.get(CONF_PORT, 6444)): cv.port, 74 | vol.Optional(CONF_TOKEN, 75 | default=user_input.get(CONF_TOKEN, "")): cv.string, 76 | vol.Optional(CONF_K1, 77 | default=user_input.get(CONF_K1, "")): cv.string, 78 | vol.Optional(CONF_PROMPT_TONE, 79 | default=user_input.get(CONF_PROMPT_TONE, True)): cv.boolean, 80 | vol.Optional(CONF_TEMP_STEP, 81 | default=user_input.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), 82 | vol.Optional(CONF_INCLUDE_OFF_AS_STATE, 83 | default=user_input.get(CONF_INCLUDE_OFF_AS_STATE, True)): cv.boolean, 84 | vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, 85 | default=user_input.get(CONF_USE_FAN_ONLY_WORKAROUND, False)): cv.boolean, 86 | vol.Optional(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, 87 | default=user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, False)): cv.boolean 88 | }) 89 | 90 | return self.async_show_form(step_id="user", data_schema=data_schema, errors=errors) 91 | 92 | async def _test_connection(self, config) -> ac | None: 93 | # Construct the device 94 | id = config.get(CONF_ID) 95 | host = config.get(CONF_HOST) 96 | port = config.get(CONF_PORT) 97 | device = ac(host, int(id), port) 98 | 99 | # Configure token and k1 as needed 100 | token = config.get(CONF_TOKEN) 101 | k1 = config.get(CONF_K1) 102 | if token and k1: 103 | success = await self.hass.async_add_executor_job(device.authenticate, k1, token) 104 | else: 105 | await self.hass.async_add_executor_job(device.refresh) 106 | success = device.online 107 | 108 | return device if success else None 109 | 110 | @staticmethod 111 | @callback 112 | def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: 113 | return MideaOptionsFlow(config_entry) 114 | 115 | 116 | class MideaOptionsFlow(OptionsFlow): 117 | 118 | def __init__(self, config_entry: ConfigEntry) -> None: 119 | self.config_entry = config_entry 120 | 121 | async def async_step_init(self, user_input=None) -> FlowResult: 122 | if user_input is not None: 123 | # Confusingly, data argument in OptionsFlow is passed to async_setup_entry in the options member 124 | return self.async_create_entry(title="", data=user_input) 125 | 126 | options = self.config_entry.options 127 | 128 | data_schema = vol.Schema({ 129 | vol.Optional(CONF_PROMPT_TONE, 130 | default=options.get(CONF_PROMPT_TONE, True)): cv.boolean, 131 | vol.Optional(CONF_TEMP_STEP, 132 | default=options.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), 133 | vol.Optional(CONF_INCLUDE_OFF_AS_STATE, 134 | default=options.get(CONF_INCLUDE_OFF_AS_STATE, True)): cv.boolean, 135 | vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, 136 | default=options.get(CONF_USE_FAN_ONLY_WORKAROUND, False)): cv.boolean, 137 | vol.Optional(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, 138 | default=options.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, False)): cv.boolean 139 | }) 140 | 141 | return self.async_show_form(step_id="init", data_schema=data_schema) 142 | -------------------------------------------------------------------------------- /custom_components/midea_ac/const.py: -------------------------------------------------------------------------------- 1 | DOMAIN = "midea_ac" 2 | 3 | CONF_K1 = "k1" 4 | CONF_PROMPT_TONE = "prompt_tone" 5 | CONF_TEMP_STEP = "temp_step" 6 | CONF_INCLUDE_OFF_AS_STATE = "include_off_as_state" 7 | CONF_USE_FAN_ONLY_WORKAROUND = "use_fan_only_workaround" 8 | CONF_KEEP_LAST_KNOWN_ONLINE_STATE = "keep_last_known_online_state" -------------------------------------------------------------------------------- /custom_components/midea_ac/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "midea_ac", 3 | "name": "Midea Smart AC", 4 | "version": "0.2.4", 5 | "documentation": "https://github.com/mac-zhou/midea-ac-py", 6 | "issue_tracker": "https://github.com/mac-zhou/midea-ac-py/issues", 7 | "requirements": ["msmart==0.2.4", "pycryptodome", "pycryptodomex", "click"], 8 | "dependencies": [], 9 | "codeowners": ["@mac-zhou"], 10 | "iot_class": "local_polling", 11 | "config_flow": true, 12 | "loggers": ["msmart"] 13 | } 14 | -------------------------------------------------------------------------------- /custom_components/midea_ac/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import TEMP_CELSIUS, CONF_ID 8 | from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass, RestoreSensor 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 11 | 12 | # Local constants 13 | from .const import DOMAIN 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | 18 | async def async_setup_entry( 19 | hass: HomeAssistant, 20 | config_entry: ConfigEntry, 21 | add_entities: AddEntitiesCallback, 22 | ) -> None: 23 | """Setup the sensor platform for Midea Smart AC.""" 24 | 25 | _LOGGER.info("Setting up sensor platform.") 26 | 27 | # Get config data from entry 28 | config = config_entry.data 29 | 30 | # Fetch device from global data 31 | id = config.get(CONF_ID) 32 | device = hass.data[DOMAIN][id] 33 | 34 | # Create sensor entities from device 35 | add_entities([ 36 | MideaTemperatureSensor(device, "indoor_temperature"), 37 | MideaTemperatureSensor(device, "outdoor_temperature"), 38 | ]) 39 | 40 | 41 | class MideaTemperatureSensor(RestoreSensor): 42 | """Temperature sensor for Midea AC.""" 43 | 44 | def __init__(self, device, prop): 45 | self._device = device 46 | self._prop = prop 47 | self._native_value = None 48 | 49 | async def async_added_to_hass(self) -> None: 50 | await super().async_added_to_hass() 51 | 52 | if (last_sensor_data := await self.async_get_last_sensor_data()) is None: 53 | return 54 | 55 | # Restore previous native value 56 | self._native_value = last_sensor_data.native_value 57 | 58 | async def async_update(self) -> None: 59 | # Grab the property from the device 60 | if self.available: 61 | self._native_value = getattr(self._device, self._prop) 62 | 63 | @property 64 | def device_info(self) -> dict: 65 | return { 66 | "identifiers": { 67 | (DOMAIN, self._device.id) 68 | }, 69 | } 70 | 71 | @property 72 | def name(self) -> str: 73 | return f"{DOMAIN}_{self._prop}_{self._device.id}" 74 | 75 | @property 76 | def unique_id(self) -> str: 77 | return f"{self._device.id}-{self._prop}" 78 | 79 | @property 80 | def available(self) -> bool: 81 | return self._device.online 82 | 83 | @property 84 | def device_class(self) -> str: 85 | return SensorDeviceClass.TEMPERATURE 86 | 87 | @property 88 | def state_class(self) -> str: 89 | return SensorStateClass.MEASUREMENT 90 | 91 | @property 92 | def native_unit_of_measurement(self) -> str: 93 | return TEMP_CELSIUS 94 | 95 | @property 96 | def native_value(self) -> float: 97 | return self._native_value 98 | -------------------------------------------------------------------------------- /custom_components/midea_ac/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configure Midea Smart AC Device", 6 | "description": "Enter information for your device.", 7 | "data": { 8 | "id": "ID", 9 | "host": "Host", 10 | "port": "Port", 11 | "token": "Token", 12 | "k1": "K1", 13 | "prompt_tone": "Prompt Tone", 14 | "temp_step": "Temperature Step", 15 | "include_off_as_state": "Include \"Off\" State", 16 | "use_fan_only_workaround": "Use Fan-only Workaround", 17 | "keep_last_known_online_state": "Keep Last Known State" 18 | }, 19 | "data_description": { 20 | "token": "Token for V3 devices", 21 | "k1": "K1 for V3 devices", 22 | "prompt_tone": "Enable the beep when sending commands", 23 | "temp_step": "Step size for temperature set point" 24 | } 25 | } 26 | }, 27 | "abort":{ 28 | "already_configured": "The device ID has already been configured." 29 | }, 30 | "error":{ 31 | "cannot_connect":"A connection could not be made with these settings." 32 | } 33 | }, 34 | "options": { 35 | "step": { 36 | "init": { 37 | "title": "Options for Midea Smart AC Device", 38 | "data": { 39 | "prompt_tone": "Prompt Tone", 40 | "temp_step": "Temperature Step", 41 | "include_off_as_state": "Include \"Off\" State", 42 | "use_fan_only_workaround": "Use Fan-only Workaround", 43 | "keep_last_known_online_state": "Keep Last Known State" 44 | }, 45 | "data_description": { 46 | "prompt_tone": "Enable the beep when sending commands", 47 | "temp_step": "Step size for temperature set point" 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /custom_components/midea_ac/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configure Midea Smart AC Device", 6 | "description": "Enter information for your device.", 7 | "data": { 8 | "id": "ID", 9 | "host": "Host", 10 | "port": "Port", 11 | "token": "Token", 12 | "k1": "K1", 13 | "prompt_tone": "Prompt Tone", 14 | "temp_step": "Temperature Step", 15 | "include_off_as_state": "Include \"Off\" State", 16 | "use_fan_only_workaround": "Use Fan-only Workaround", 17 | "keep_last_known_online_state": "Keep Last Known State" 18 | }, 19 | "data_description": { 20 | "token": "Token for V3 devices", 21 | "k1": "K1 for V3 devices", 22 | "prompt_tone": "Enable the beep when sending commands", 23 | "temp_step": "Step size for temperature set point" 24 | } 25 | } 26 | }, 27 | "abort":{ 28 | "already_configured": "The device ID has already been configured." 29 | }, 30 | "error":{ 31 | "cannot_connect":"A connection could not be made with these settings." 32 | } 33 | }, 34 | "options": { 35 | "step": { 36 | "init": { 37 | "title": "Options for Midea Smart AC Device", 38 | "data": { 39 | "prompt_tone": "Prompt Tone", 40 | "temp_step": "Temperature Step", 41 | "include_off_as_state": "Include \"Off\" State", 42 | "use_fan_only_workaround": "Use Fan-only Workaround", 43 | "keep_last_known_online_state": "Keep Last Known State" 44 | }, 45 | "data_description": { 46 | "prompt_tone": "Enable the beep when sending commands", 47 | "temp_step": "Step size for temperature set point" 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Midea Smart Aircon", 4 | "domains": ["climate"], 5 | "render_readme": false, 6 | "homeassistant": "0.110.2" 7 | } -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | Home Assistant Custom Integration for Midea Group(Hualing, Senville, Klimaire, AirCon, Century, Pridiom, Thermocore, Comfee, Alpine Home Air, Artel, Beko, Electrolux, Galactic, Idea, Inventor, Kaisai, Mitsui, Mr. Cool, Neoclima, Olimpia Splendid, Pioneer, QLIMA, Royal Clima, Qzen, Toshiba, Carrier, Goodman, Friedrich, Samsung, Kenmore, Trane, Lennox, LG and much more) Air Conditioners via LAN. 2 | 3 | Tested with Home Assistant 2021.7.2. 4 | 5 | ## Attention!!! 6 | Version >= 0.1.27, the device naming rules have changed. 7 | 8 | **Example configuration.yaml:** 9 | 10 | ```yaml 11 | climate: 12 | platform: midea_ac 13 | host: midea_device_ip_address 14 | id: midea_device_applianceId 15 | ``` 16 | 17 | ## For more information, please visit [midea-ac-py](https://github.com/mac-zhou/midea-ac-py) 18 | 19 | ## Buy me a cup of coffee 20 | 21 | - [via Paypal](https://www.paypal.me/himaczhou) 22 | - [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) 23 | - [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) 24 | - [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) 25 | 26 | Your donation will make me work better for this project. -------------------------------------------------------------------------------- /pcap-decrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import argparse 4 | import json 5 | import pprint 6 | import ipaddress 7 | import pyshark 8 | from msmart.lan import lan 9 | from msmart.command import appliance_response 10 | from msmart.security import security 11 | 12 | 13 | def convert_device_id_int(device_id: str): 14 | old = bytearray.fromhex(device_id) 15 | new = reversed(old) 16 | return int(bytearray(new).hex(), 16) 17 | 18 | 19 | def get_type(i: int): 20 | if i == 32: # 0x20 21 | return 'get' 22 | elif i == 34: # 0x22 23 | return 'reply' 24 | elif i == 35: # 0x23 25 | return 'set' 26 | else: 27 | return 'unknown' 28 | 29 | 30 | def get_operational_mode(i: int): 31 | # auto = 1 cool = 2 dry = 3 heat = 4 fan_only = 5 32 | if i == 1: 33 | return 'Auto' 34 | elif i == 2: 35 | return 'Cool' 36 | elif i == 3: 37 | return 'Dry' 38 | elif i == 4: 39 | return 'Heat' 40 | elif i == 5: 41 | return 'Fan_only' 42 | else: 43 | return 'Unknown' 44 | 45 | 46 | def get_fan_speed(i: int): 47 | # Auto = 101 102 High = 80 Medium = 60 Low = 40 Silent = 20 48 | if i == 101 or i == 102: 49 | return 'Auto' 50 | elif i == 80: 51 | return 'High' 52 | elif i == 60: 53 | return 'Medium' 54 | elif i == 40: 55 | return 'Low' 56 | elif i == 20: 57 | return 'Silent' 58 | else: 59 | return 'unknown' 60 | 61 | 62 | parser = argparse.ArgumentParser( 63 | description=( 64 | "Decipher Midea's Msmart local binary protocol from " 65 | "Wireshark / pcap-ng captures")) 66 | parser.add_argument("pcapfile", type=str, help="path to pcapng dump") 67 | 68 | parser.add_argument('-f', "--fiter_type", type=str, dest="fiter_type", help='fliter type from midea message', 69 | default='all', choices=['all', 'get', 'reply', 'set', 'unknown', 'error']) 70 | parser.add_argument("--tcp-raw", action='store_true') 71 | parser.add_argument("--msg-raw", action='store_true') 72 | args = parser.parse_args() 73 | cap = pyshark.FileCapture( 74 | args.pcapfile, display_filter=("tcp && data.len == 104 && data[:2] == 5a5a")) 75 | 76 | _security = security() 77 | 78 | for packet in cap: 79 | packet.data.raw_mode = True 80 | tcp_data = packet.data.data 81 | tcp_data_bytes = bytearray.fromhex(tcp_data) 82 | tcp_data_len = int(tcp_data_bytes[4:5].hex(), 16) 83 | if len(tcp_data_bytes) != tcp_data_len: 84 | continue 85 | device_id = tcp_data_bytes[20:26].hex() 86 | midea_data = tcp_data_bytes[40:88] 87 | reply = _security.aes_decrypt(midea_data) 88 | 89 | msg_type_hex = 255 90 | msg_type = 'error' 91 | 92 | if len(reply) >= 20: 93 | msg_type_hex = reply[1] 94 | msg_type = get_type(msg_type_hex) 95 | 96 | if args.fiter_type != 'all': 97 | if msg_type != args.fiter_type: 98 | continue 99 | 100 | print("\n### No.{0} {1} {2} => {3}".format( 101 | packet.number, packet.sniff_time, packet.ip.src, packet.ip.dst)) 102 | if (not ipaddress.ip_address(packet.ip.src).is_private 103 | or not ipaddress.ip_address(packet.ip.dst).is_private): 104 | print("NOT LOCAL: packet to/from Midea Cloud") 105 | 106 | print("Message Type:\t %s %s applianceId: -hex: %s -int: %d" % 107 | (msg_type.upper(), hex(msg_type_hex), device_id, convert_device_id_int(device_id))) 108 | 109 | if len(reply) >= 20: 110 | response = appliance_response(reply) 111 | print("Decoded Data:\t {}".format({ 112 | 'power_state': response.power_state, 113 | 'operational_mode': get_operational_mode(response.operational_mode), 114 | 'target_temperature': response.target_temperature, 115 | 'fan_speed': get_fan_speed(response.fan_speed), 116 | 'swing_mode': response.swing_mode, 117 | 'eco_mode': response.eco_mode, 118 | 'turbo_mode': response.turbo_mode, 119 | 'indoor_temperature': response.indoor_temperature, 120 | 'outdoor_temperature': response.outdoor_temperature, 121 | })) 122 | else: 123 | print("Decoded Data:\t Can't Decoded") 124 | 125 | if args.tcp_raw: 126 | print("TCP RAW:\t %s" % tcp_data) 127 | if args.msg_raw: 128 | print("Message RAW:\t %s" % reply.hex()) 129 | --------------------------------------------------------------------------------