├── .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 │ └── manifest.json ├── hacs.json ├── info.md ├── pcap-decrypt.py └── 中文.md /.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 | 2 | English Version | [中文版](./中文.md#) 3 | 4 | This is a custom component for Home Assistant to integrate the Midea Air Conditioners via the Local area network. 5 | 6 | Tested with hass version 0.110.2 7 | 8 | ## Installation 9 | 10 | ### Install from HACS 11 | [![Type](https://img.shields.io/badge/Type-Custom_Component-orange.svg)](https://github.com/dlarrick/hass-kumo) [![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 | key | description | example 23 | :--- | :--- | :--- 24 | **platform (Required)** | The platform name. | midea_ac 25 | **host (Required)** | Midea AC Device's IP Address. | 192.168.1.100 26 | **id (Required)** | Midea AC Device's applianceId. | 123456789012345 27 | **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 28 | 29 | **How to Get applianceId:** 30 | 31 | - you can use command ```midea-discover``` to discover midea devices on the host in the same Local area network. Note: This component only supports devices with model 0xac (air conditioner) and words ```supported``` in the output. 32 | ```shell 33 | pip3 install msmart 34 | midea-discover 35 | ``` 36 | 37 | - if you use Midea Air app outside China, there is a easy way to get your deviceid. 38 | 39 | 1. open Midea Air app, and share the device, you will get a QR Code. 40 | 2. save the QR Code 41 | 3. upload QR Code Sreenshort to https://zxing.org/w/decode.jspx or decode QR code use other tool. 42 | 4. you will get the data like MADEVICESHARE: 43 | 5. decode base64 string online https://www.base64decode.org/ or use other tool 44 | 6. you will get the device id 45 | 46 | - if you use android, you can use ```adb```,filter from log: 47 | ```shell 48 | adb logcat | grep -i deviceid 49 | ``` 50 | 51 | - if you use iPhone,iPhone connects to macOS with a data cable and filters the applianceId from the console log 52 | 53 | - If you do not have the above environment and conditions, you need to capture the air conditioner and save the files, after can be used [pcap-decrypt.py](./pcap-decrypt.py#) to Get. Remember to use the number, not hex string. 54 | 55 | **Example configuration.yaml:** 56 | * Single device 57 | ```yaml 58 | climate: 59 | - platform: midea_ac 60 | host: 192.168.1.100 61 | id: 123456789012345 62 | ``` 63 | * Multiple device 64 | ```yaml 65 | climate: 66 | - platform: midea_ac 67 | host: 192.168.1.100 68 | id: 123456789012345 69 | - platform: midea_ac 70 | host: 192.168.1.200 71 | id: 543210987654321 72 | ``` 73 | 74 | ## Buy me a cup of coffee to help maintain this project further? 75 | 76 | - [via Paypal](https://www.paypal.me/himaczhou) 77 | - [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) 78 | - [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) 79 | - [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) 80 | 81 | Your donation will make me work better for this project. 82 | -------------------------------------------------------------------------------- /custom_components/midea_ac/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WMP/midea-ac-py/7cb1cfe37352e11f35796a0743e39954d152dc2e/custom_components/midea_ac/__init__.py -------------------------------------------------------------------------------- /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 | import logging 10 | 11 | import voluptuous as vol 12 | from datetime import timedelta 13 | 14 | import homeassistant.helpers.config_validation as cv 15 | from homeassistant.components.climate import PLATFORM_SCHEMA 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.const import CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, TEMP_FAHRENHEIT, \ 24 | ATTR_TEMPERATURE 25 | 26 | from homeassistant.helpers.restore_state import RestoreEntity 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | CONF_HOST = 'host' 31 | CONF_ID = 'id' 32 | CONF_TEMP_STEP = 'temp_step' 33 | CONF_INCLUDE_OFF_AS_STATE = 'include_off_as_state' 34 | CONF_USE_FAN_ONLY_WORKAROUND = 'use_fan_only_workaround' 35 | 36 | SCAN_INTERVAL = timedelta(seconds=15) 37 | 38 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 39 | vol.Required(CONF_HOST): cv.string, 40 | vol.Required(CONF_ID): cv.string, 41 | vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), 42 | vol.Optional(CONF_INCLUDE_OFF_AS_STATE, default=True): vol.Coerce(bool), 43 | vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, default=False): vol.Coerce(bool) 44 | }) 45 | 46 | SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE \ 47 | | SUPPORT_SWING_MODE | SUPPORT_PRESET_MODE 48 | 49 | 50 | async def async_setup_platform(hass, config, async_add_entities, 51 | discovery_info=None): 52 | """Set up the Midea cloud service and query appliances.""" 53 | 54 | from msmart.device import device as midea_device 55 | 56 | device_ip = config.get(CONF_HOST) 57 | device_id = config.get(CONF_ID) 58 | temp_step = config.get(CONF_TEMP_STEP) 59 | include_off_as_state = config.get(CONF_INCLUDE_OFF_AS_STATE) 60 | use_fan_only_workaround = config.get(CONF_USE_FAN_ONLY_WORKAROUND) 61 | 62 | client = midea_device(device_ip, int(device_id)) 63 | device = client.setup() 64 | entities = [] 65 | entities.append(MideaClimateACDevice( 66 | hass, device, temp_step, include_off_as_state, 67 | use_fan_only_workaround)) 68 | 69 | async_add_entities(entities) 70 | 71 | 72 | class MideaClimateACDevice(ClimateEntity, RestoreEntity): 73 | """Representation of a Midea climate AC device.""" 74 | 75 | def __init__(self, hass, device, temp_step: float, 76 | include_off_as_state: bool, use_fan_only_workaround: bool): 77 | """Initialize the climate device.""" 78 | from msmart.device import air_conditioning_device as ac 79 | 80 | self._operation_list = ac.operational_mode_enum.list() 81 | self._fan_list = ac.fan_speed_enum.list() 82 | self._swing_list = ac.swing_mode_enum.list() 83 | if include_off_as_state: 84 | self._operation_list.append("off") 85 | self._support_flags = SUPPORT_FLAGS 86 | #the LED display on the AC should use the same unit as that in homeassistant 87 | device.farenheit_unit = (hass.config.units.temperature_unit == TEMP_FAHRENHEIT) 88 | self._device = device 89 | self._unit_of_measurement = TEMP_CELSIUS 90 | self._target_temperature_step = temp_step 91 | self._include_off_as_state = include_off_as_state 92 | self._use_fan_only_workaround = use_fan_only_workaround 93 | 94 | self.hass = hass 95 | self._old_state = None 96 | self._changed = False 97 | 98 | async def apply_changes(self): 99 | if not self._changed: 100 | return 101 | await self.hass.async_add_executor_job(self._device.apply) 102 | self._old_state = None 103 | await self.async_update_ha_state() 104 | self._changed = False 105 | 106 | async def async_update(self): 107 | """Retrieve latest state from the appliance if no changes made, 108 | otherwise update the remote device state.""" 109 | if self._changed: 110 | await self.hass.async_add_executor_job(self._device.apply) 111 | self._changed = False 112 | elif not self._use_fan_only_workaround: 113 | self._old_state = None 114 | await self.hass.async_add_executor_job(self._device.refresh) 115 | 116 | async def async_added_to_hass(self): 117 | """Run when entity about to be added.""" 118 | await super().async_added_to_hass() 119 | self._old_state = await self.async_get_last_state() 120 | 121 | @property 122 | def state_attributes(self): 123 | attrs = super().state_attributes 124 | attrs["outdoor_temperature"] = self._device.outdoor_temperature 125 | 126 | return attrs 127 | 128 | @property 129 | def available(self): 130 | """Checks if the appliance is available for commands.""" 131 | return self._device.online 132 | 133 | @property 134 | def supported_features(self): 135 | """Return the list of supported features.""" 136 | return self._support_flags 137 | 138 | @property 139 | def target_temperature_step(self): 140 | """Return the supported step of target temperature.""" 141 | return self._target_temperature_step 142 | 143 | @property 144 | def hvac_modes(self): 145 | """Return the list of available operation modes.""" 146 | return self._operation_list 147 | 148 | @property 149 | def fan_modes(self): 150 | """Return the list of available fan modes.""" 151 | return self._fan_list 152 | 153 | @property 154 | def swing_modes(self): 155 | """List of available swing modes.""" 156 | return self._swing_list 157 | 158 | @property 159 | def assumed_state(self): 160 | """Assume state rather than refresh to workaround fan_only bug.""" 161 | return self._use_fan_only_workaround 162 | 163 | @property 164 | def should_poll(self): 165 | """Poll the appliance for changes, there is no notification capability in the Midea API""" 166 | return not self._use_fan_only_workaround 167 | 168 | @property 169 | def unique_id(self): 170 | return self._device.id 171 | 172 | @property 173 | def name(self): 174 | """Return the name of the climate device.""" 175 | return "midea_ac_{}".format(self._device.id) 176 | 177 | @property 178 | def temperature_unit(self): 179 | """Return the unit of measurement.""" 180 | return self._unit_of_measurement 181 | 182 | @property 183 | def current_temperature(self): 184 | """Return the current temperature.""" 185 | if self._old_state is not None: 186 | return self._old_state.attributes.get('current_temperature') 187 | 188 | return self._device.indoor_temperature 189 | 190 | @property 191 | def target_temperature(self): 192 | """Return the temperature we try to reach.""" 193 | if self._old_state is not None and 'temperature' in self._old_state.attributes: 194 | self._device.target_temperature = self._old_state.attributes['temperature'] 195 | return self._old_state.attributes['temperature'] 196 | 197 | return self._device.target_temperature 198 | 199 | @property 200 | def hvac_mode(self): 201 | """Return current operation ie. heat, cool, idle.""" 202 | if self._old_state is not None: 203 | from msmart.device import air_conditioning_device as ac 204 | self._device.power_state = self._include_off_as_state and self._old_state.state != 'off' 205 | if self._old_state.state in ac.operational_mode_enum.list(): 206 | self._device.operational_mode = ac.operational_mode_enum[self._old_state.state] 207 | return self._old_state.state 208 | 209 | if self._include_off_as_state and not self._device.power_state: 210 | return "off" 211 | return self._device.operational_mode.name 212 | 213 | @property 214 | def fan_mode(self): 215 | """Return the fan setting.""" 216 | if self._old_state is not None and 'fan_mode' in self._old_state.attributes: 217 | from msmart.device import air_conditioning_device as ac 218 | self._device.fan_speed = ac.fan_speed_enum[self._old_state.attributes['fan_mode']] 219 | return self._old_state.attributes['fan_mode'] 220 | 221 | return self._device.fan_speed.name 222 | 223 | @property 224 | def swing_mode(self): 225 | """Return the swing setting.""" 226 | if self._old_state is not None and 'swing_mode' in self._old_state.attributes: 227 | from msmart.device import air_conditioning_device as ac 228 | self._device.swing_mode = ac.swing_mode_enum[self._old_state.attributes['swing_mode']] 229 | return self._old_state.attributes['swing_mode'] 230 | 231 | return self._device.swing_mode.name 232 | 233 | @property 234 | def is_on(self): 235 | """Return true if the device is on.""" 236 | return self._device.power_state 237 | 238 | async def async_set_temperature(self, **kwargs): 239 | """Set new target temperatures.""" 240 | if kwargs.get(ATTR_TEMPERATURE) is not None: 241 | self._device.target_temperature = (kwargs.get(ATTR_TEMPERATURE)) 242 | self._changed = True 243 | await self.apply_changes() 244 | 245 | async def async_set_swing_mode(self, swing_mode): 246 | """Set new target temperature.""" 247 | from msmart.device import air_conditioning_device as ac 248 | self._device.swing_mode = ac.swing_mode_enum[swing_mode] 249 | self._changed = True 250 | await self.apply_changes() 251 | 252 | async def async_set_fan_mode(self, fan_mode): 253 | """Set new target temperature.""" 254 | from msmart.device import air_conditioning_device as ac 255 | self._device.fan_speed = ac.fan_speed_enum[fan_mode] 256 | self._changed = True 257 | await self.apply_changes() 258 | 259 | async def async_set_hvac_mode(self, hvac_mode): 260 | """Set new target temperature.""" 261 | from msmart.device import air_conditioning_device as ac 262 | if self._include_off_as_state and hvac_mode == "off": 263 | self._device.power_state = False 264 | else: 265 | if self._include_off_as_state: 266 | self._device.power_state = True 267 | self._device.operational_mode = ac.operational_mode_enum[hvac_mode] 268 | self._changed = True 269 | await self.apply_changes() 270 | 271 | async def async_set_preset_mode(self, preset_mode: str): 272 | if preset_mode == PRESET_NONE: 273 | self._device.eco_mode = False 274 | self._device.turbo_mode = False 275 | elif preset_mode == PRESET_BOOST: 276 | self._device.eco_mode = False 277 | self._device.turbo_mode = True 278 | elif preset_mode == PRESET_ECO: 279 | self._device.turbo_mode = False 280 | self._device.eco_mode = True 281 | 282 | self._changed = True 283 | await self.apply_changes() 284 | 285 | @property 286 | def preset_modes(self): 287 | return [PRESET_NONE, PRESET_ECO, PRESET_BOOST] 288 | 289 | @property 290 | def preset_mode(self): 291 | if self._old_state is not None and 'preset_mode' in self._old_state.attributes: 292 | preset_mode = self._old_state.attributes['preset_mode'] 293 | if preset_mode == PRESET_ECO: 294 | self._device.eco_mode = True 295 | self._device.turbo_mode = False 296 | elif preset_mode == PRESET_BOOST: 297 | self._device.turbo_mode = True 298 | self._device.eco_mode = False 299 | 300 | return preset_mode 301 | 302 | if self._device.eco_mode: 303 | return PRESET_ECO 304 | elif self._device.turbo_mode: 305 | return PRESET_BOOST 306 | else: 307 | return PRESET_NONE 308 | 309 | async def async_turn_on(self): 310 | """Turn on.""" 311 | self._device.power_state = True 312 | self._changed = True 313 | await self.apply_changes() 314 | 315 | async def async_turn_off(self): 316 | """Turn off.""" 317 | self._device.power_state = False 318 | self._changed = True 319 | await self.apply_changes() 320 | 321 | @property 322 | def min_temp(self): 323 | """Return the minimum temperature.""" 324 | return 17 325 | 326 | @property 327 | def max_temp(self): 328 | """Return the maximum temperature.""" 329 | return 30 330 | -------------------------------------------------------------------------------- /custom_components/midea_ac/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "midea_ac", 3 | "name": "Midea Smart Aircon", 4 | "documentation": "", 5 | "requirements": ["msmart==0.1.20", "pycryptodome", "midea"], 6 | "dependencies": [], 7 | "codeowners": ["@mac-zhou"] 8 | } 9 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Midea Smart Aircon", 4 | "domains": ["climate"], 5 | "render_readme": false, 6 | "homeassistant": "0.108.0" 7 | } -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | This is a custom component for Home Assistant to integrate the Midea Air Conditioners via the Local area network. 2 | 3 | Tested with hass version 0.110.2 4 | 5 | **Example configuration.yaml:** 6 | 7 | ```yaml 8 | climate: 9 | platform: midea_ac 10 | host: midea_device_ip_address 11 | id: midea_device_applianceId 12 | ``` 13 | 14 | **Configuration variables:** 15 | 16 | key | description | example 17 | :--- | :--- | :--- 18 | **platform (Required)** | The platform name. | midea_ac 19 | **host (Required)** | Midea AC Device's IP Address. | 192.168.1.100 20 | **id (Required)** | Midea AC Device's applianceId. | 123456789012345 21 | **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 22 | 23 | **How to Get applianceId:** 24 | 25 | - you can use command ```midea-discover``` to discover midea devices on the host in the same Local area network. Note: This component only supports devices with model 0xac (air conditioner) and words ```supported``` in the output. 26 | ```shell 27 | pip3 install msmart 28 | midea-discover 29 | ``` 30 | 31 | - if you use Midea Air app outside China, there is a easy way to get your deviceid. 32 | 33 | 1. open Midea Air app, and share the device, you will get a QR Code. 34 | 2. save the QR Code 35 | 3. upload QR Code Sreenshort to https://zxing.org/w/decode.jspx or decode QR code use other tool. 36 | 4. you will get the data like MADEVICESHARE: 37 | 5. decode base64 string online https://www.base64decode.org/ or use other tool 38 | 6. you will get the device id 39 | 40 | - if you use android, you can use ```adb```,filter from log: 41 | ```shell 42 | adb logcat | grep -i deviceid 43 | ``` 44 | 45 | - if you use iPhone,iPhone connects to macOS with a data cable and filters the applianceId from the console log 46 | 47 | - If you do not have the above environment and conditions, you need to capture the air conditioner and save the files, after can be used [pcap-decrypt.py](./pcap-decrypt.py#) to Get. Remember to use the number, not hex string. 48 | 49 | **Example configuration.yaml:** 50 | * Single device 51 | ```yaml 52 | climate: 53 | - platform: midea_ac 54 | host: 192.168.1.100 55 | id: 123456789012345 56 | ``` 57 | * Multiple device 58 | ```yaml 59 | climate: 60 | - platform: midea_ac 61 | host: 192.168.1.100 62 | id: 123456789012345 63 | - platform: midea_ac 64 | host: 192.168.1.200 65 | id: 543210987654321 66 | ``` 67 | 68 | ## Buy me a cup of coffee to help maintain this project further? 69 | 70 | - [via Paypal](https://www.paypal.me/himaczhou) 71 | - [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) 72 | - [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) 73 | - [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) 74 | 75 | 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 | -------------------------------------------------------------------------------- /中文.md: -------------------------------------------------------------------------------- 1 | 2 | 美的空调的Home Assistant插件,通过局域网来控制设备。 3 | 4 | Tested with hass version 0.110.2 5 | 6 | ## Installation (安装) 7 | 8 | ### Install from HACS (HACS商店安装) 9 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 10 | 11 | 在HACS中搜索 ```midea_ac``` 12 | 13 | ### Install manually (手工安装) 14 | 1. 克隆此仓库 15 | 2. 将 `custom_components/midea_ac` 目录复制到你的 `custom_components` 目录下 16 | 17 | ## Configuration (配置) 18 | 19 | **Configuration variables (配置变量说明):** 20 | 参数 | 说明 | 示例 21 | :--- | :--- | :--- 22 | **platform (必填)** | 插件名称 | midea_ac 23 | **host (必填)** | 美的空调的IP地址 | 192.168.1.100 24 | **id (必填)** | 美的空调的applianceId. | 123456789012345 25 | **use_fan_only_workaround (可选)** | Set this to true if you need to turn off device updates because they turn device on and to fan_only | true 26 | 27 | **How to Get applianceId(如何获取设备id):** 28 | 29 | - 你可以在同一网络下的设备上运行 midea-discover 命令去发现相关midea的设备。注意:插件仅支持型号为0xac(空调)且输出中有 ```supported``` 字样的设备。 30 | ```shell 31 | pip3 install msmart 32 | midea-discover 33 | ``` 34 | 35 | - 如果你使用安卓手机,可以使用adb logcat命令,从log中获取: 36 | ```shell 37 | adb logcat | grep -i deviceid 38 | ``` 39 | 40 | - 如果你使用iPhone和macOS设备的话,可以将iPhone用数据线连到macOS,从控制台log中筛选applianceId 41 | 42 | - 如果你都不具备以上环境,你需要对空调进行抓包,抓包后的文件可以用[pcap-decrypt.py](./pcap-decrypt.py#)从中获取,记得使用数字,不是hex字符 43 | 44 | **Example configuration.yaml (配置文件示例) :** 45 | * 单台设备 46 | ```yaml 47 | climate: 48 | - platform: midea_ac 49 | host: 192.168.1.100 50 | id: 123456789012345 51 | ``` 52 | * 多台设备 53 | ```yaml 54 | climate: 55 | - platform: midea_ac 56 | host: 192.168.1.100 57 | id: 123456789012345 58 | - platform: midea_ac 59 | host: 192.168.1.200 60 | id: 543210987654321 61 | ``` 62 | 63 | ## 赞赏一下, 请我喝杯咖啡? 64 | 65 | - [via Paypal](https://www.paypal.me/himaczhou) 66 | - [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) 67 | - [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) 68 | - [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) 69 | --------------------------------------------------------------------------------