diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 77e5822..8a08523 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,31 +1,20 @@ --- -name: Bug report -about: Something is not working right +name: Bug report (错误报告) +about: Something is not working right (运行不正常) title: '' labels: bug assignees: '' --- -**Describe the bug** +**Describe the bug (描述一下问题)** A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +**Screenshots / Logs / Pcap File(屏幕截图/日志/抓包文件)** +If applicable, add screenshots or your home-assistant log file or pcap file to help explain your problem. +请提交相关截图,日志,抓包让开发者能够更快的解决问题 -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots / Logs** -If applicable, add screenshots or your home-assistant log file to help explain your problem. - -**Versions** +**Versions (版本信息)** - Home Assistant version: - - Midea integration version (commit hash): + - Midea msmart version: -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491..524a440 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: Feature request (功能需求) about: Suggest an idea for this project title: '' labels: enhancement @@ -7,14 +7,9 @@ assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** +**Describe the solution you'd like (描述需求)** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. +**Screenshots / Logs / Pcap File(屏幕截图/日志/抓包文件)** +If applicable, add screenshots or your home-assistant log file or pcap file to help explain your problem. +请提交相关截图,日志,抓包让开发者能够更快的解决问题 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 894a44c..3fe5230 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ venv.bak/ # mypy .mypy_cache/ +.vscode-upload.json diff --git a/LICENSE b/LICENSE index 2531200..02fedd6 100644 --- a/LICENSE +++ b/LICENSE @@ -2,6 +2,7 @@ MIT License Copyright (c) 2018 NeoAcheron Copyright (c) 2019 Josh Anderson (@andersonshatch) +Copyright (c) 2020 Mac Zhou (@mac-zhou) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index bfa433a..f08c72e 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,89 @@ -Updated home-assistant component for hass version 0.96 and the "climate-1.0" changes. +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. -Last tested with hass version 0.108.x +Tested with Home Assistant 2021.7.2. + +## Attention!!! +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. ## Installation -### HACS [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) -1. Search the HACS Store for Midea -2. Install the Midea Aircon component -3. Add configuration to your yaml, as shown here: https://github.com/NeoAcheron/midea-ac-py/wiki/Installing-to-Home-Assistant +### Install from HACS +[![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) + +Search the HACS Store for ```midea_ac``` -### Manual +### Install manually 1. Clone this repo -1. Place the `custom_components/midea` folder into your `custom_components` folder -1. Add configuration to your yaml, as shown here: https://github.com/NeoAcheron/midea-ac-py/wiki/Installing-to-Home-Assistant +2. Place the `custom_components/midea_ac` folder into your `custom_components` folder -## Fan Only Workaround -There is an optional workaround to avoid reading the device's state on initial connection, since for some reason the update method causes my device to turn on and be set to fan only mode. (This is a bug to be fixed in [andersonshatch/midea-ac-lib](https://github.com/andersonshatch/midea-ac-lib)... if only I knew how.) +## Configuration -With this workaround enabled, it restores state from home-assistant's previous state. -This should work okay as long as you only alter the state of your device using home-assisant. +**Configuration variables:** -If you find this component turns your device on and to fan_only every time home-assistant updates it (once a minute by default), you probably want to turn on the workaround with this config property: -``` -use_fan_only_workaround: true +key | description | example +:--- | :--- | :--- +**platform (Required)** | The platform name. | midea_ac +**host (Required)** | Midea AC Device's IP Address. | 192.168.1.100 +**id (Required)** | Midea AC Device's applianceId. | 123456789012345 +**token (Optional)** | Midea AC Device's token, V3 is required. | ACEDDA53831AE5DC...(Length 128) +**k1 (Optional)** | Midea AC Device's k1, V3 is required. | CFFA10FC...(Length 64) +**temp_step (Optional)** | Step size for temperature set point, default is 1.0 | 0.5 +**prompt_tone (Optional)** | Prompt Tone, default is true. | false +**keep_last_known_online_state (Optional)** | Set this to true if you see too many `unavailable` in log. | true +**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 + +**Example configuration.yaml:** +```yaml +climate: + - platform: midea_ac + host: 192.168.1.100 + id: 123456789012345 + # v3 need token and k1(key) + # token: ACEDDA53831AE5DC...(Length 128) + # k1: CFFA10FC...(Length 64) ``` -Original Readme: -```# midea-ac-py +## How to Get Configuration variables: +- `midea-discover` can help you discover Midea devices from the LAN. + ```zsh + pip3 install msmart + midea-discover + ``` + - Basic Usage + ``` + Usage: midea-discover [OPTIONS] -This is a library to allow communicating to a Midea AC via the Midea Cloud. + Discover Midea Deivces and Get Device's info -This is a very early release, and comes without any guarantees. This is still an early work in progress and simply serves as a proof of concept. + Options: + -d, --debug Enable debug logging + -c, --amount INTEGER Number of broadcast packets, default is 1. + if you have many devices, you may change this value. + -a, --account TEXT Your email address for your Midea account. + -p, --password TEXT Your password for your Midea account. + -i, --ip TEXT IP address of Midea device. you can use: + - broadcasts don't work. + - just get one device's info. + - an error occurred. + --help Show this message and exit. + ``` + ***Note***: + - This component only supports devices with model 0xac (air conditioner) and words `supported` in the output. + - Configure v3 devices need `token` and `k1`. + - You `midea-discover` when broadcasts don't work. + - `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). + it's my account, but now it’s an open account. + If you just only get Token and K1(key) with this account, I and others can't get your information through this account. + 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. + ```zsh + midea-discover -a YOUR_ACCOUNT -p YOUR_PASSWORD + ``` -This library would not have been possible if it wasn't for the amazing work done by @yitsushi and his Ruby based command line tool. -You can find his work here: https://github.com/yitsushi/midea-air-condition -The reasons for me converting this to Python is that this library also serves as a platform component for Home Assistant. +## Buy me a cup of coffee -## Wiki -Please visit the Wiki for device support and instruction on how to use this component: https://github.com/NeoAcheron/midea-ac-py/wiki -``` +- [via Paypal](https://www.paypal.me/himaczhou) +- [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) +- [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) +- [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) + +Your donation will make me work better for this project. diff --git a/custom_components/midea/__init__.py b/custom_components/midea/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/custom_components/midea/climate.py b/custom_components/midea/climate.py deleted file mode 100644 index 34c6b4c..0000000 --- a/custom_components/midea/climate.py +++ /dev/null @@ -1,326 +0,0 @@ -""" -A climate platform that adds support for Midea air conditioning units. - -For more details about this platform, please refer to the documentation -https://github.com/andersonshatch/midea-ac-py - -This is still early work in progress -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA -from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_PRESET_MODE, PRESET_NONE, PRESET_ECO, PRESET_BOOST) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, \ - ATTR_TEMPERATURE - -from homeassistant.helpers.restore_state import RestoreEntity - -_LOGGER = logging.getLogger(__name__) - -CONF_APP_KEY = 'app_key' -CONF_TEMP_STEP = 'temp_step' -CONF_INCLUDE_OFF_AS_STATE = 'include_off_as_state' -CONF_USE_FAN_ONLY_WORKAROUND = 'use_fan_only_workaround' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_APP_KEY): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), - vol.Optional(CONF_INCLUDE_OFF_AS_STATE, default=True): vol.Coerce(bool), - vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, default=False): vol.Coerce(bool) -}) - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE \ - | SUPPORT_SWING_MODE | SUPPORT_PRESET_MODE - - -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the Midea cloud service and query appliances.""" - - from midea.client import client as midea_client - - app_key = config.get(CONF_APP_KEY) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - temp_step = config.get(CONF_TEMP_STEP) - include_off_as_state = config.get(CONF_INCLUDE_OFF_AS_STATE) - use_fan_only_workaround = config.get(CONF_USE_FAN_ONLY_WORKAROUND) - - client = midea_client(app_key, username, password) - devices = client.devices() - entities = [] - for device in devices: - if device.type == 0xAC: - entities.append(MideaClimateACDevice( - hass, device, temp_step, include_off_as_state, - use_fan_only_workaround)) - else: - _LOGGER.error( - "Unsupported device type: 0x{:02x}".format(device.type)) - - async_add_entities(entities) - - -class MideaClimateACDevice(ClimateDevice, RestoreEntity): - """Representation of a Midea climate AC device.""" - - def __init__(self, hass, device, temp_step: float, - include_off_as_state: bool, use_fan_only_workaround: bool): - """Initialize the climate device.""" - from midea.device import air_conditioning_device as ac - - self._operation_list = ac.operational_mode_enum.list() - self._fan_list = ac.fan_speed_enum.list() - self._swing_list = ac.swing_mode_enum.list() - if include_off_as_state: - self._operation_list.append("off") - self._support_flags = SUPPORT_FLAGS - self._device = device - self._unit_of_measurement = TEMP_CELSIUS - self._target_temperature_step = temp_step - self._include_off_as_state = include_off_as_state - self._use_fan_only_workaround = use_fan_only_workaround - - self.hass = hass - self._old_state = None - self._changed = False - - async def apply_changes(self): - if not self._changed: - return - await self.hass.async_add_executor_job(self._device.apply) - self._old_state = None - await self.async_update_ha_state() - self._changed = False - - async def async_update(self): - """Retrieve latest state from the appliance if no changes made, - otherwise update the remote device state.""" - if self._changed: - await self.hass.async_add_executor_job(self._device.apply) - self._changed = False - elif not self._use_fan_only_workaround: - self._old_state = None - await self.hass.async_add_executor_job(self._device.refresh) - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - await super().async_added_to_hass() - self._old_state = await self.async_get_last_state() - - @property - def state_attributes(self): - attrs = super().state_attributes - attrs["outdoor_temperature"] = self._device.outdoor_temperature - - return attrs - - @property - def available(self): - """Checks if the appliance is available for commands.""" - return self._device.online - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self._target_temperature_step - - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return self._operation_list - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return self._fan_list - - @property - def swing_modes(self): - """List of available swing modes.""" - return self._swing_list - - @property - def assumed_state(self): - """Assume state rather than refresh to workaround fan_only bug.""" - return self._use_fan_only_workaround - - @property - def should_poll(self): - """Poll the appliance for changes, there is no notification capability in the Midea API""" - return not self._use_fan_only_workaround - - @property - def unique_id(self): - return self._device.id - - @property - def name(self): - """Return the name of the climate device.""" - return "midea_{}".format(self._device.id) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def current_temperature(self): - """Return the current temperature.""" - if self._old_state is not None: - return self._old_state.attributes.get('current_temperature') - - return self._device.indoor_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self._old_state is not None and 'temperature' in self._old_state.attributes: - self._device.target_temperature = self._old_state.attributes['temperature'] - return self._old_state.attributes['temperature'] - - return self._device.target_temperature - - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - if self._old_state is not None: - from midea.device import air_conditioning_device as ac - self._device.power_state = self._include_off_as_state and self._old_state.state != 'off' - if self._old_state.state in ac.operational_mode_enum.list(): - self._device.operational_mode = ac.operational_mode_enum[self._old_state.state] - return self._old_state.state - - if self._include_off_as_state and not self._device.power_state: - return "off" - return self._device.operational_mode.name - - @property - def fan_mode(self): - """Return the fan setting.""" - if self._old_state is not None and 'fan_mode' in self._old_state.attributes: - from midea.device import air_conditioning_device as ac - self._device.fan_speed = ac.fan_speed_enum[self._old_state.attributes['fan_mode']] - return self._old_state.attributes['fan_mode'] - - return self._device.fan_speed.name - - @property - def swing_mode(self): - """Return the swing setting.""" - if self._old_state is not None and 'swing_mode' in self._old_state.attributes: - from midea.device import air_conditioning_device as ac - self._device.swing_mode = ac.swing_mode_enum[self._old_state.attributes['swing_mode']] - return self._old_state.attributes['swing_mode'] - - return self._device.swing_mode.name - - @property - def is_on(self): - """Return true if the device is on.""" - return self._device.power_state - - async def async_set_temperature(self, **kwargs): - """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - self._device.target_temperature = int(kwargs.get(ATTR_TEMPERATURE)) - self._changed = True - await self.apply_changes() - - async def async_set_swing_mode(self, swing_mode): - """Set new target temperature.""" - from midea.device import air_conditioning_device as ac - self._device.swing_mode = ac.swing_mode_enum[swing_mode] - self._changed = True - await self.apply_changes() - - async def async_set_fan_mode(self, fan_mode): - """Set new target temperature.""" - from midea.device import air_conditioning_device as ac - self._device.fan_speed = ac.fan_speed_enum[fan_mode] - self._changed = True - await self.apply_changes() - - async def async_set_hvac_mode(self, hvac_mode): - """Set new target temperature.""" - from midea.device import air_conditioning_device as ac - if self._include_off_as_state and hvac_mode == "off": - self._device.power_state = False - else: - if self._include_off_as_state: - self._device.power_state = True - self._device.operational_mode = ac.operational_mode_enum[hvac_mode] - self._changed = True - await self.apply_changes() - - async def async_set_preset_mode(self, preset_mode: str): - if preset_mode == PRESET_NONE: - self._device.eco_mode = False - self._device.turbo_mode = False - elif preset_mode == PRESET_BOOST: - self._device.eco_mode = False - self._device.turbo_mode = True - elif preset_mode == PRESET_ECO: - self._device.turbo_mode = False - self._device.eco_mode = True - - self._changed = True - await self.apply_changes() - - @property - def preset_modes(self): - return [PRESET_NONE, PRESET_ECO, PRESET_BOOST] - - @property - def preset_mode(self): - if self._old_state is not None and 'preset_mode' in self._old_state.attributes: - preset_mode = self._old_state.attributes['preset_mode'] - if preset_mode == PRESET_ECO: - self._device.eco_mode = True - self._device.turbo_mode = False - elif preset_mode == PRESET_BOOST: - self._device.turbo_mode = True - self._device.eco_mode = False - - return preset_mode - - if self._device.eco_mode: - return PRESET_ECO - elif self._device.turbo_mode: - return PRESET_BOOST - else: - return PRESET_NONE - - async def async_turn_on(self): - """Turn on.""" - self._device.power_state = True - self._changed = True - await self.apply_changes() - - async def async_turn_off(self): - """Turn off.""" - self._device.power_state = False - self._changed = True - await self.apply_changes() - - @property - def min_temp(self): - """Return the minimum temperature.""" - return 17 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 30 diff --git a/custom_components/midea/manifest.json b/custom_components/midea/manifest.json deleted file mode 100644 index d553f12..0000000 --- a/custom_components/midea/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "midea", - "name": "Midea Aircon", - "documentation": "", - "requirements": ["midea-andersonshatch==0.1.11", "pycryptodome"], - "dependencies": [], - "codeowners": ["@andersonshatch"], - "homeassistant": "0.96.0" -} diff --git a/custom_components/midea_ac/__init__.py b/custom_components/midea_ac/__init__.py new file mode 100644 index 0000000..aeaed71 --- /dev/null +++ b/custom_components/midea_ac/__init__.py @@ -0,0 +1,74 @@ +"""Integration for Midea Smart AC.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN +from homeassistant.core import HomeAssistant +from msmart.device import air_conditioning as ac + +# Local constants +from .const import ( + DOMAIN, + CONF_K1 +) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up a Midea AC device from a config entry.""" + + # Ensure the global data dict exists + hass.data.setdefault(DOMAIN, {}) + + # Get config data from entry + config = config_entry.data + + # Attempt to get device from global data + id = config.get(CONF_ID) + device = hass.data[DOMAIN].get(id) + + # Construct a new device if necessary + if device is None: + # Construct the device + id = config.get(CONF_ID) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + device = ac(host, int(id), port) + + # Configure token and k1 as needed + token = config.get(CONF_TOKEN) + k1 = config.get(CONF_K1) + if token and k1: + await hass.async_add_executor_job(device.authenticate, k1, token) + + hass.data[DOMAIN][id] = device + + # Create platform entries + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "climate")) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor")) + + # Reload entry when its updated + config_entry.async_on_unload( + config_entry.add_update_listener(async_reload_entry)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + + # Get config data from entry + config = config_entry.data + + # Remove device from global data + id = config.get(CONF_ID) + hass.data[DOMAIN].pop(id) + + await hass.config_entries.async_forward_entry_unload(config_entry, "climate") + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + return True + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/custom_components/midea_ac/climate.py b/custom_components/midea_ac/climate.py new file mode 100644 index 0000000..37723cb --- /dev/null +++ b/custom_components/midea_ac/climate.py @@ -0,0 +1,306 @@ +""" +A climate platform that adds support for Midea air conditioning units. + +For more details about this platform, please refer to the documentation +https://github.com/mac-zhou/midea-ac-py + +This is still early work in progress +""" +from __future__ import annotations + +import datetime +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_ID +try: + from homeassistant.components.climate import ClimateEntity +except ImportError: + from homeassistant.components.climate import ClimateDevice as ClimateEntity +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, + SUPPORT_PRESET_MODE, PRESET_NONE, PRESET_ECO, PRESET_BOOST) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from msmart.device import air_conditioning as ac + +# Local constants +from .const import ( + DOMAIN, + CONF_PROMPT_TONE, + CONF_TEMP_STEP, + CONF_INCLUDE_OFF_AS_STATE, + CONF_USE_FAN_ONLY_WORKAROUND, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE +) + +_LOGGER = logging.getLogger(__name__) + +# Override default scan interval? +SCAN_INTERVAL = datetime.timedelta(seconds=15) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Setup the climate platform for Midea Smart AC.""" + + _LOGGER.info("Setting up climate platform.") + + # Get config and options data from entry + config = config_entry.data + options = config_entry.options + + # Fetch device from global data + id = config.get(CONF_ID) + device = hass.data[DOMAIN][id] + + # Query device capabilities + _LOGGER.info("Querying device capabilities.") + await hass.async_add_executor_job(device.get_capabilities) + + add_entities([ + MideaClimateACDevice(hass, device, options) + ]) + + +class MideaClimateACDevice(ClimateEntity): + """Representation of a Midea climate AC device.""" + + def __init__(self, hass, device, options: dict): + """Initialize the climate device.""" + + self.hass = hass + self._device = device + + # Apply options + self._device.prompt_tone = options.get(CONF_PROMPT_TONE) + self._device.keep_last_known_online_state = options.get( + CONF_KEEP_LAST_KNOWN_ONLINE_STATE) + + # Display on the AC should use the same unit as homeassistant + self._device.fahrenheit = ( + hass.config.units.temperature_unit == TEMP_FAHRENHEIT) + + self._target_temperature_step = options.get(CONF_TEMP_STEP) + self._include_off_as_state = options.get(CONF_INCLUDE_OFF_AS_STATE) + self._use_fan_only_workaround = options.get( + CONF_USE_FAN_ONLY_WORKAROUND) + + self._operation_list = device.supported_operation_modes + if self._include_off_as_state: + self._operation_list.append("off") + + self._fan_list = ac.fan_speed_enum.list() + self._swing_list = device.supported_swing_modes + + self._changed = False + + async def apply_changes(self) -> None: + if not self._changed: + return + await self.hass.async_add_executor_job(self._device.apply) + await self.async_update_ha_state() + self._changed = False + + async def async_update(self) -> None: + """Retrieve latest state from the appliance if no changes made, + otherwise update the remote device state.""" + if self._changed: + await self.hass.async_add_executor_job(self._device.apply) + self._changed = False + elif not self._use_fan_only_workaround: + await self.hass.async_add_executor_job(self._device.refresh) + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added.""" + await super().async_added_to_hass() + + # Populate data ASAP + await self.async_update() + + @property + def device_info(self) -> dict: + return { + "identifiers": { + (DOMAIN, self._device.id) + }, + "name": self.name, + "manufacturer": "Midea", + } + + @property + def available(self) -> bool: + """Checks if the appliance is available for commands.""" + return self._device.online + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE | SUPPORT_PRESET_MODE + + @property + def target_temperature_step(self) -> float: + """Return the supported step of target temperature.""" + return self._target_temperature_step + + @property + def hvac_modes(self) -> list: + """Return the list of available operation modes.""" + return self._operation_list + + @property + def fan_modes(self) -> list: + """Return the list of available fan modes.""" + return self._fan_list + + @property + def swing_modes(self) -> list: + """List of available swing modes.""" + return self._swing_list + + @property + def assumed_state(self) -> bool: + """Assume state rather than refresh to workaround fan_only bug.""" + return self._use_fan_only_workaround + + @property + def should_poll(self) -> bool: + """Poll the appliance for changes, there is no notification capability in the Midea API""" + return not self._use_fan_only_workaround + + @property + def unique_id(self) -> str: + return f"{self._device.id}" + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return f"{DOMAIN}_{self._device.id}" + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return self._device.indoor_temperature + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._device.target_temperature + + @property + def hvac_mode(self) -> str: + """Return current operation ie. heat, cool, idle.""" + if self._include_off_as_state and not self._device.power_state: + return "off" + return self._device.operational_mode.name + + @property + def fan_mode(self) -> str: + """Return the fan setting.""" + return self._device.fan_speed.name + + @property + def swing_mode(self) -> str: + """Return the swing setting.""" + return self._device.swing_mode.name + + @property + def is_on(self) -> bool: + """Return true if the device is on.""" + return self._device.power_state + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperatures.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + # grab temperature from front end UI + temp = kwargs.get(ATTR_TEMPERATURE) + + # round temperature to nearest .5 + temp = round(temp * 2) / 2 + + # send temperature to unit + self._device.target_temperature = temp + self._changed = True + await self.apply_changes() + + async def async_set_swing_mode(self, swing_mode) -> None: + """Set swing mode.""" + self._device.swing_mode = ac.swing_mode_enum[swing_mode] + self._changed = True + await self.apply_changes() + + async def async_set_fan_mode(self, fan_mode) -> None: + """Set fan mode.""" + """Fix key error when calling from HomeKit""" + fan_mode = fan_mode.capitalize() + self._device.fan_speed = ac.fan_speed_enum[fan_mode] + self._changed = True + await self.apply_changes() + + async def async_set_hvac_mode(self, hvac_mode) -> None: + """Set hvac mode.""" + if self._include_off_as_state and hvac_mode == "off": + self._device.power_state = False + else: + if self._include_off_as_state: + self._device.power_state = True + self._device.operational_mode = ac.operational_mode_enum[hvac_mode] + self._changed = True + await self.apply_changes() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + if preset_mode == PRESET_NONE: + self._device.eco_mode = False + self._device.turbo_mode = False + elif preset_mode == PRESET_BOOST: + self._device.eco_mode = False + self._device.turbo_mode = True + elif preset_mode == PRESET_ECO: + self._device.turbo_mode = False + self._device.eco_mode = True + + self._changed = True + await self.apply_changes() + + @property + def preset_modes(self) -> list: + return [PRESET_NONE, PRESET_ECO, PRESET_BOOST] + + @property + def preset_mode(self) -> str: + if self._device.eco_mode: + return PRESET_ECO + elif self._device.turbo_mode: + return PRESET_BOOST + else: + return PRESET_NONE + + async def async_turn_on(self) -> None: + """Turn on.""" + self._device.power_state = True + self._changed = True + await self.apply_changes() + + async def async_turn_off(self) -> None: + """Turn off.""" + self._device.power_state = False + self._changed = True + await self.apply_changes() + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return 17 + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return 30 diff --git a/custom_components/midea_ac/config_flow.py b/custom_components/midea_ac/config_flow.py new file mode 100644 index 0000000..b0b63ae --- /dev/null +++ b/custom_components/midea_ac/config_flow.py @@ -0,0 +1,141 @@ +"""Config flow for Midea Smart AC.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from msmart.device import air_conditioning as ac +import voluptuous as vol + +# Local constants +from .const import ( + DOMAIN, + CONF_K1, + CONF_PROMPT_TONE, + CONF_TEMP_STEP, + CONF_INCLUDE_OFF_AS_STATE, + CONF_USE_FAN_ONLY_WORKAROUND, + CONF_KEEP_LAST_KNOWN_ONLINE_STATE +) + + +class MideaConfigFlow(ConfigFlow, domain=DOMAIN): + + async def async_step_user(self, user_input) -> FlowResult: + errors = {} + if user_input is not None: + # Set the unique ID and abort if duplicate exists + id = user_input.get(CONF_ID) + await self.async_set_unique_id(id) + self._abort_if_unique_id_configured() + + # Attempt a connection to see if config is valid + device = await self._test_connection(user_input) + + if device: + # Save the device into global data + self.hass.data.setdefault(DOMAIN, {}) + self.hass.data[DOMAIN][id] = device + + # Split user input config data and options + data = { + CONF_ID: id, + CONF_HOST: user_input.get(CONF_HOST), + CONF_PORT: user_input.get(CONF_PORT), + CONF_TOKEN: user_input.get(CONF_TOKEN), + CONF_K1: user_input.get(CONF_K1), + } + options = { + CONF_PROMPT_TONE: user_input.get(CONF_PROMPT_TONE), + CONF_TEMP_STEP: user_input.get(CONF_TEMP_STEP), + CONF_INCLUDE_OFF_AS_STATE: user_input.get(CONF_INCLUDE_OFF_AS_STATE), + CONF_USE_FAN_ONLY_WORKAROUND: user_input.get(CONF_USE_FAN_ONLY_WORKAROUND), + CONF_KEEP_LAST_KNOWN_ONLINE_STATE: user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE), + } + + # Create a config entry with the config data and options + return self.async_create_entry(title=f"{DOMAIN} {id}", data=data, options=options) + else: + # Indicate a connection could not be made + errors["base"] = "cannot_connect" + + + user_input = user_input or {} + + data_schema = vol.Schema({ + vol.Required(CONF_ID, + default=user_input.get(CONF_ID)): cv.string, + vol.Required(CONF_HOST, + default=user_input.get(CONF_HOST)): cv.string, + vol.Optional(CONF_PORT, + default=user_input.get(CONF_PORT, 6444)): cv.port, + vol.Optional(CONF_TOKEN, + default=user_input.get(CONF_TOKEN, "")): cv.string, + vol.Optional(CONF_K1, + default=user_input.get(CONF_K1, "")): cv.string, + vol.Optional(CONF_PROMPT_TONE, + default=user_input.get(CONF_PROMPT_TONE, True)): cv.boolean, + vol.Optional(CONF_TEMP_STEP, + default=user_input.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), + vol.Optional(CONF_INCLUDE_OFF_AS_STATE, + default=user_input.get(CONF_INCLUDE_OFF_AS_STATE, True)): cv.boolean, + vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, + default=user_input.get(CONF_USE_FAN_ONLY_WORKAROUND, False)): cv.boolean, + vol.Optional(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, + default=user_input.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, False)): cv.boolean + }) + + return self.async_show_form(step_id="user", data_schema=data_schema, errors=errors) + + async def _test_connection(self, config) -> ac | None: + # Construct the device + id = config.get(CONF_ID) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + device = ac(host, int(id), port) + + # Configure token and k1 as needed + token = config.get(CONF_TOKEN) + k1 = config.get(CONF_K1) + if token and k1: + success = await self.hass.async_add_executor_job(device.authenticate, k1, token) + else: + await self.hass.async_add_executor_job(device.refresh) + success = device.online + + return device if success else None + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + return MideaOptionsFlow(config_entry) + + +class MideaOptionsFlow(OptionsFlow): + + def __init__(self, config_entry: ConfigEntry) -> None: + self.config_entry = config_entry + + async def async_step_init(self, user_input=None) -> FlowResult: + if user_input is not None: + # Confusingly, data argument in OptionsFlow is passed to async_setup_entry in the options member + return self.async_create_entry(title="", data=user_input) + + options = self.config_entry.options + + data_schema = vol.Schema({ + vol.Optional(CONF_PROMPT_TONE, + default=options.get(CONF_PROMPT_TONE, True)): cv.boolean, + vol.Optional(CONF_TEMP_STEP, + default=options.get(CONF_TEMP_STEP, 1.0)): vol.All(vol.Coerce(float), vol.Range(min=0.5, max=5)), + vol.Optional(CONF_INCLUDE_OFF_AS_STATE, + default=options.get(CONF_INCLUDE_OFF_AS_STATE, True)): cv.boolean, + vol.Optional(CONF_USE_FAN_ONLY_WORKAROUND, + default=options.get(CONF_USE_FAN_ONLY_WORKAROUND, False)): cv.boolean, + vol.Optional(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, + default=options.get(CONF_KEEP_LAST_KNOWN_ONLINE_STATE, False)): cv.boolean + }) + + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/custom_components/midea_ac/const.py b/custom_components/midea_ac/const.py new file mode 100644 index 0000000..b56d232 --- /dev/null +++ b/custom_components/midea_ac/const.py @@ -0,0 +1,8 @@ +DOMAIN = "midea_ac" + +CONF_K1 = "k1" +CONF_PROMPT_TONE = "prompt_tone" +CONF_TEMP_STEP = "temp_step" +CONF_INCLUDE_OFF_AS_STATE = "include_off_as_state" +CONF_USE_FAN_ONLY_WORKAROUND = "use_fan_only_workaround" +CONF_KEEP_LAST_KNOWN_ONLINE_STATE = "keep_last_known_online_state" \ No newline at end of file diff --git a/custom_components/midea_ac/manifest.json b/custom_components/midea_ac/manifest.json new file mode 100644 index 0000000..db91376 --- /dev/null +++ b/custom_components/midea_ac/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "midea_ac", + "name": "Midea Smart AC", + "version": "0.2.4", + "documentation": "https://github.com/mac-zhou/midea-ac-py", + "issue_tracker": "https://github.com/mac-zhou/midea-ac-py/issues", + "requirements": ["msmart==0.2.4", "pycryptodome", "pycryptodomex", "click"], + "dependencies": [], + "codeowners": ["@mac-zhou"], + "iot_class": "local_polling", + "config_flow": true, + "loggers": ["msmart"] +} diff --git a/custom_components/midea_ac/sensor.py b/custom_components/midea_ac/sensor.py new file mode 100644 index 0000000..926f315 --- /dev/null +++ b/custom_components/midea_ac/sensor.py @@ -0,0 +1,97 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS, CONF_ID +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass, RestoreSensor +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +# Local constants +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Setup the sensor platform for Midea Smart AC.""" + + _LOGGER.info("Setting up sensor platform.") + + # Get config data from entry + config = config_entry.data + + # Fetch device from global data + id = config.get(CONF_ID) + device = hass.data[DOMAIN][id] + + # Create sensor entities from device + add_entities([ + MideaTemperatureSensor(device, "indoor_temperature"), + MideaTemperatureSensor(device, "outdoor_temperature"), + ]) + + +class MideaTemperatureSensor(RestoreSensor): + """Temperature sensor for Midea AC.""" + + def __init__(self, device, prop): + self._device = device + self._prop = prop + self._native_value = None + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + if (last_sensor_data := await self.async_get_last_sensor_data()) is None: + return + + # Restore previous native value + self._native_value = last_sensor_data.native_value + + async def async_update(self) -> None: + # Grab the property from the device + if self.available: + self._native_value = getattr(self._device, self._prop) + + @property + def device_info(self) -> dict: + return { + "identifiers": { + (DOMAIN, self._device.id) + }, + } + + @property + def name(self) -> str: + return f"{DOMAIN}_{self._prop}_{self._device.id}" + + @property + def unique_id(self) -> str: + return f"{self._device.id}-{self._prop}" + + @property + def available(self) -> bool: + return self._device.online + + @property + def device_class(self) -> str: + return SensorDeviceClass.TEMPERATURE + + @property + def state_class(self) -> str: + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str: + return TEMP_CELSIUS + + @property + def native_value(self) -> float: + return self._native_value diff --git a/custom_components/midea_ac/strings.json b/custom_components/midea_ac/strings.json new file mode 100644 index 0000000..231472e --- /dev/null +++ b/custom_components/midea_ac/strings.json @@ -0,0 +1,52 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Midea Smart AC Device", + "description": "Enter information for your device.", + "data": { + "id": "ID", + "host": "Host", + "port": "Port", + "token": "Token", + "k1": "K1", + "prompt_tone": "Prompt Tone", + "temp_step": "Temperature Step", + "include_off_as_state": "Include \"Off\" State", + "use_fan_only_workaround": "Use Fan-only Workaround", + "keep_last_known_online_state": "Keep Last Known State" + }, + "data_description": { + "token": "Token for V3 devices", + "k1": "K1 for V3 devices", + "prompt_tone": "Enable the beep when sending commands", + "temp_step": "Step size for temperature set point" + } + } + }, + "abort":{ + "already_configured": "The device ID has already been configured." + }, + "error":{ + "cannot_connect":"A connection could not be made with these settings." + } + }, + "options": { + "step": { + "init": { + "title": "Options for Midea Smart AC Device", + "data": { + "prompt_tone": "Prompt Tone", + "temp_step": "Temperature Step", + "include_off_as_state": "Include \"Off\" State", + "use_fan_only_workaround": "Use Fan-only Workaround", + "keep_last_known_online_state": "Keep Last Known State" + }, + "data_description": { + "prompt_tone": "Enable the beep when sending commands", + "temp_step": "Step size for temperature set point" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/midea_ac/translations/en.json b/custom_components/midea_ac/translations/en.json new file mode 100644 index 0000000..231472e --- /dev/null +++ b/custom_components/midea_ac/translations/en.json @@ -0,0 +1,52 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Midea Smart AC Device", + "description": "Enter information for your device.", + "data": { + "id": "ID", + "host": "Host", + "port": "Port", + "token": "Token", + "k1": "K1", + "prompt_tone": "Prompt Tone", + "temp_step": "Temperature Step", + "include_off_as_state": "Include \"Off\" State", + "use_fan_only_workaround": "Use Fan-only Workaround", + "keep_last_known_online_state": "Keep Last Known State" + }, + "data_description": { + "token": "Token for V3 devices", + "k1": "K1 for V3 devices", + "prompt_tone": "Enable the beep when sending commands", + "temp_step": "Step size for temperature set point" + } + } + }, + "abort":{ + "already_configured": "The device ID has already been configured." + }, + "error":{ + "cannot_connect":"A connection could not be made with these settings." + } + }, + "options": { + "step": { + "init": { + "title": "Options for Midea Smart AC Device", + "data": { + "prompt_tone": "Prompt Tone", + "temp_step": "Temperature Step", + "include_off_as_state": "Include \"Off\" State", + "use_fan_only_workaround": "Use Fan-only Workaround", + "keep_last_known_online_state": "Keep Last Known State" + }, + "data_description": { + "prompt_tone": "Enable the beep when sending commands", + "temp_step": "Step size for temperature set point" + } + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..66db7e1 --- /dev/null +++ b/hacs.json @@ -0,0 +1,7 @@ + +{ + "name": "Midea Smart Aircon", + "domains": ["climate"], + "render_readme": false, + "homeassistant": "0.110.2" +} \ No newline at end of file diff --git a/info.md b/info.md index cf24a5c..b0e41c5 100644 --- a/info.md +++ b/info.md @@ -1,19 +1,26 @@ +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. + +Tested with Home Assistant 2021.7.2. + +## Attention!!! +Version >= 0.1.27, the device naming rules have changed. + **Example configuration.yaml:** ```yaml climate: - platform: midea - app_key: midea_app_api_key - username: 'foo@bar.com' - password: !secret midea_password + platform: midea_ac + host: midea_device_ip_address + id: midea_device_applianceId ``` -**Configuration variables:** - -key | description -:--- | :--- -**platform (Required)** | The platform name. -**app_key (Required)** | Midea app API key. -**username (Required)** | Username for Midea cloud. -**password (Required)** | Password for Midea cloud. -**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 +## For more information, please visit [midea-ac-py](https://github.com/mac-zhou/midea-ac-py) + +## Buy me a cup of coffee + +- [via Paypal](https://www.paypal.me/himaczhou) +- [via Bitcoin](bitcoin:3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy) (**3GAvud4ZcppF5xeTPEqF9FcX2buvTsi2Hy**) +- [via AliPay(支付宝)](https://i.loli.net/2020/05/08/nNSTAPUGDgX2sBe.png) +- [via WeChatPay(微信)](https://i.loli.net/2020/05/08/ouj6SdnVirDzRw9.jpg) + +Your donation will make me work better for this project. \ No newline at end of file diff --git a/pcap-decrypt.py b/pcap-decrypt.py new file mode 100644 index 0000000..ce53c62 --- /dev/null +++ b/pcap-decrypt.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +import sys +import argparse +import json +import pprint +import ipaddress +import pyshark +from msmart.lan import lan +from msmart.command import appliance_response +from msmart.security import security + + +def convert_device_id_int(device_id: str): + old = bytearray.fromhex(device_id) + new = reversed(old) + return int(bytearray(new).hex(), 16) + + +def get_type(i: int): + if i == 32: # 0x20 + return 'get' + elif i == 34: # 0x22 + return 'reply' + elif i == 35: # 0x23 + return 'set' + else: + return 'unknown' + + +def get_operational_mode(i: int): + # auto = 1 cool = 2 dry = 3 heat = 4 fan_only = 5 + if i == 1: + return 'Auto' + elif i == 2: + return 'Cool' + elif i == 3: + return 'Dry' + elif i == 4: + return 'Heat' + elif i == 5: + return 'Fan_only' + else: + return 'Unknown' + + +def get_fan_speed(i: int): + # Auto = 101 102 High = 80 Medium = 60 Low = 40 Silent = 20 + if i == 101 or i == 102: + return 'Auto' + elif i == 80: + return 'High' + elif i == 60: + return 'Medium' + elif i == 40: + return 'Low' + elif i == 20: + return 'Silent' + else: + return 'unknown' + + +parser = argparse.ArgumentParser( + description=( + "Decipher Midea's Msmart local binary protocol from " + "Wireshark / pcap-ng captures")) +parser.add_argument("pcapfile", type=str, help="path to pcapng dump") + +parser.add_argument('-f', "--fiter_type", type=str, dest="fiter_type", help='fliter type from midea message', + default='all', choices=['all', 'get', 'reply', 'set', 'unknown', 'error']) +parser.add_argument("--tcp-raw", action='store_true') +parser.add_argument("--msg-raw", action='store_true') +args = parser.parse_args() +cap = pyshark.FileCapture( + args.pcapfile, display_filter=("tcp && data.len == 104 && data[:2] == 5a5a")) + +_security = security() + +for packet in cap: + packet.data.raw_mode = True + tcp_data = packet.data.data + tcp_data_bytes = bytearray.fromhex(tcp_data) + tcp_data_len = int(tcp_data_bytes[4:5].hex(), 16) + if len(tcp_data_bytes) != tcp_data_len: + continue + device_id = tcp_data_bytes[20:26].hex() + midea_data = tcp_data_bytes[40:88] + reply = _security.aes_decrypt(midea_data) + + msg_type_hex = 255 + msg_type = 'error' + + if len(reply) >= 20: + msg_type_hex = reply[1] + msg_type = get_type(msg_type_hex) + + if args.fiter_type != 'all': + if msg_type != args.fiter_type: + continue + + print("\n### No.{0} {1} {2} => {3}".format( + packet.number, packet.sniff_time, packet.ip.src, packet.ip.dst)) + if (not ipaddress.ip_address(packet.ip.src).is_private + or not ipaddress.ip_address(packet.ip.dst).is_private): + print("NOT LOCAL: packet to/from Midea Cloud") + + print("Message Type:\t %s %s applianceId: -hex: %s -int: %d" % + (msg_type.upper(), hex(msg_type_hex), device_id, convert_device_id_int(device_id))) + + if len(reply) >= 20: + response = appliance_response(reply) + print("Decoded Data:\t {}".format({ + 'power_state': response.power_state, + 'operational_mode': get_operational_mode(response.operational_mode), + 'target_temperature': response.target_temperature, + 'fan_speed': get_fan_speed(response.fan_speed), + 'swing_mode': response.swing_mode, + 'eco_mode': response.eco_mode, + 'turbo_mode': response.turbo_mode, + 'indoor_temperature': response.indoor_temperature, + 'outdoor_temperature': response.outdoor_temperature, + })) + else: + print("Decoded Data:\t Can't Decoded") + + if args.tcp_raw: + print("TCP RAW:\t %s" % tcp_data) + if args.msg_raw: + print("Message RAW:\t %s" % reply.hex())