From 8aa22143063b61ef10d06cc24ffc6978a9ad9a62 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 19 Mar 2018 08:07:44 +0100 Subject: [PATCH 01/39] Target temperature properly named and swing mode uses the enum now --- custom_components/climate/xiaomi_miio.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 1cd53f5..3b61cee 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -203,23 +203,17 @@ def async_update(self): ATTR_AIR_CONDITION_MODEL: state.air_condition_model, ATTR_LOAD_POWER: state.load_power, ATTR_TEMPERATURE: state.temperature, - # TODO: Refactor swing_mode - ATTR_SWING_MODE: SwingMode.On.name if state.swing_mode else SwingMode.Off.name, + ATTR_SWING_MODE: state.swing_mode.name, ATTR_FAN_SPEED: state.fan_speed.name, ATTR_OPERATION_MODE: state.mode.name, ATTR_LED: state.led, }) self._current_operation = state.mode.name.lower() - # FIXME: The property is called "target_temperature" with python-miio 0.3.9 - self._target_temperature = state.temperature + self._target_temperature = state.target_temperature self._current_fan_mode = state.fan_speed.name - self._current_swing_mode = \ - SwingMode.On.name if state.swing_mode else SwingMode.Off.name - - if not self._sensor_entity_id: - self._current_temperature = state.temperature + self._current_swing_mode = state.swing_mode.name if self._air_condition_model is None: self._air_condition_model = state.air_condition_model From 3fc47b608f98834128444ce7705b5e6a6aaf77bb Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 19 Mar 2018 08:08:58 +0100 Subject: [PATCH 02/39] Never use the target temperature as current temperature --- custom_components/climate/xiaomi_miio.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 1cd53f5..c31a320 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -218,9 +218,6 @@ def async_update(self): self._current_swing_mode = \ SwingMode.On.name if state.swing_mode else SwingMode.Off.name - if not self._sensor_entity_id: - self._current_temperature = state.temperature - if self._air_condition_model is None: self._air_condition_model = state.air_condition_model From 0112afb6cee32ab06c6775837da2feb4e0032cf2 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 25 Mar 2018 23:27:34 +0200 Subject: [PATCH 03/39] Typo fixed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 901eec3..e603be0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all ## Setup ```yaml -# confugration.yaml +# configuration.yaml climate - platform: xiaomi_miio From a42c0fce4678886ae21d467a5a781107cef1db7e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Mar 2018 08:12:15 +0200 Subject: [PATCH 04/39] python-miio 0.3.9 was released --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 3b61cee..64e9bd9 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio>=0.3.9'] DEPENDENCIES = ['sensor'] From e733cc237e8301e4f25a607b326eca0ba69390d9 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 2 Apr 2018 08:57:23 +0200 Subject: [PATCH 05/39] Updated all occurrences of the new property name "target_temperature" (Closes: #12) --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 64e9bd9..c9de248 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -202,7 +202,7 @@ def async_update(self): self._state_attrs.update({ ATTR_AIR_CONDITION_MODEL: state.air_condition_model, ATTR_LOAD_POWER: state.load_power, - ATTR_TEMPERATURE: state.temperature, + ATTR_TEMPERATURE: state.target_temperature, ATTR_SWING_MODE: state.swing_mode.name, ATTR_FAN_SPEED: state.fan_speed.name, ATTR_OPERATION_MODE: state.mode.name, From 98a16c22998a90d527b42de2e4881b5796ab26c0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 18 Apr 2018 12:33:16 +0200 Subject: [PATCH 06/39] Filter unavailable/unknown sensor measurements --- custom_components/climate/xiaomi_miio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index c9de248..67b9dc6 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -138,6 +138,9 @@ def __init__(self, hass, name, device, unique_id, sensor_entity_id, @callback def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" + if state.state is None or state.state == 'unknown': + return + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: From c0ca731c7ba284272393f33b5b2d8a10efe2207e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 29 Apr 2018 10:25:07 +0200 Subject: [PATCH 07/39] Supported devices clarified (Closes: #16) --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e603be0..51414d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Xiaomi Mi and Aqara Air Conditioning Companion -This is a custom component for home assistant to integrate the Xiaomi Mi and Aqara Air Conditioning Companion (KTBL01LM, KTBL02LM). +This is a custom component for home assistant to integrate the Xiaomi Mi and Aqara Air Conditioning Companion: + +| Model ID | Model number | Product name | Shape | +|-------------------|--------------|-----------------------------------------|----------| +| `acpartner.v1` | KTBL01LM | Aqara Air Conditioning Companion | square | +| `acaprtner.v2` | KTBL02LM | Xiaomi Mi Air Conditioner Companion | round | +| `acpartner.v3` | KTBL11LM | Aqara Air Conditioning Companion | square | Please follow the instructions on [Retrieving the Access Token](https://home-assistant.io/components/xiaomi/#retrieving-the-access-token) to get the API token to use in the configuration.yaml file. From f131d3c29d3317d3e4523cc63dc2d91f8f88f85f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 29 Apr 2018 10:28:54 +0200 Subject: [PATCH 08/39] Current operation fixed --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 67b9dc6..7991b84 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -212,7 +212,7 @@ def async_update(self): ATTR_LED: state.led, }) - self._current_operation = state.mode.name.lower() + self._current_operation = state.mode.name self._target_temperature = state.target_temperature self._current_fan_mode = state.fan_speed.name From c66982f46d7d3cf24932b88700bb8de3ae34d606 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 3 Jun 2018 18:45:44 +0200 Subject: [PATCH 09/39] Bump python-miio version (Closes: #21) --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 7991b84..7df727e 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-miio>=0.3.9'] +REQUIREMENTS = ['python-miio>=0.4.0'] DEPENDENCIES = ['sensor'] From 000b03cfdad50c6b59a54b20c7510430e2d561fa Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 3 Jun 2018 18:48:37 +0200 Subject: [PATCH 10/39] Add missing colon (Closes: #19) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51414d5..a2e3d0f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all ```yaml # configuration.yaml -climate +climate: - platform: xiaomi_miio name: Aqara Air Conditioning Companion host: 192.168.130.71 From 04f864064574fc9be8f5eff9f6cc76dada72ac4e Mon Sep 17 00:00:00 2001 From: Cong Nguyen Date: Fri, 29 Jun 2018 23:10:19 +0700 Subject: [PATCH 11/39] Fix ac_model being bytes and break recorder (#22) --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 7df727e..caf636b 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -203,7 +203,7 @@ def async_update(self): self._available = True self._state = state.is_on self._state_attrs.update({ - ATTR_AIR_CONDITION_MODEL: state.air_condition_model, + ATTR_AIR_CONDITION_MODEL: state.air_condition_model.hex(), ATTR_LOAD_POWER: state.load_power, ATTR_TEMPERATURE: state.target_temperature, ATTR_SWING_MODE: state.swing_mode.name, From 2debfc83ab0a2a76513fbaa70213c9c08bc2d966 Mon Sep 17 00:00:00 2001 From: CryKiller3 Date: Sun, 8 Jul 2018 13:51:47 +0300 Subject: [PATCH 12/39] Fix #25 and bytes concatenation issue (#28) (Closes: #25) --- custom_components/climate/xiaomi_miio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index caf636b..ad9c9d6 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -219,7 +219,7 @@ def async_update(self): self._current_swing_mode = state.swing_mode.name if self._air_condition_model is None: - self._air_condition_model = state.air_condition_model + self._air_condition_model = state.air_condition_model.hex() except DeviceException as ex: self._available = False @@ -364,7 +364,7 @@ def _send_configuration(self): self._air_condition_model, Power(int(self._state)), OperationMode[self._current_operation], - self._target_temperature, + int(self._target_temperature), FanSpeed[self._current_fan_mode], SwingMode[self._current_swing_mode], Led.Off, From 04b0aee16e1a76d61e68e02aebf943a6eebc1ce2 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 21 Aug 2018 14:10:29 +0200 Subject: [PATCH 13/39] Update "Retrieve the access token" link (Closes: #32) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2e3d0f..b37c897 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is a custom component for home assistant to integrate the Xiaomi Mi and Aqa | `acaprtner.v2` | KTBL02LM | Xiaomi Mi Air Conditioner Companion | round | | `acpartner.v3` | KTBL11LM | Aqara Air Conditioning Companion | square | -Please follow the instructions on [Retrieving the Access Token](https://home-assistant.io/components/xiaomi/#retrieving-the-access-token) to get the API token to use in the configuration.yaml file. +Please follow the instructions on [Retrieving the Access Token](https://www.home-assistant.io/components/vacuum.xiaomi_miio/#retrieving-the-access-token) to get the API token to use in the configuration.yaml file. Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all the work. From 41c5d7943e4bfa8806b8042186f7a46cbe67a697 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 21 Aug 2018 14:30:57 +0200 Subject: [PATCH 14/39] Refactor mode objects and allow case-insensitive modes (Closes: #29) --- custom_components/climate/xiaomi_miio.py | 33 ++++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index ad9c9d6..17df420 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -212,11 +212,11 @@ def async_update(self): ATTR_LED: state.led, }) - self._current_operation = state.mode.name + self._current_operation = state.mode self._target_temperature = state.target_temperature - self._current_fan_mode = state.fan_speed.name - self._current_swing_mode = state.swing_mode.name + self._current_fan_mode = state.fan_speed + self._current_swing_mode = state.swing_mode if self._air_condition_model is None: self._air_condition_model = state.air_condition_model.hex() @@ -288,7 +288,7 @@ def target_temperature(self): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation + return self._current_operation.name @property def operation_list(self): @@ -299,7 +299,7 @@ def operation_list(self): @property def current_fan_mode(self): """Return the current fan mode.""" - return self._current_fan_mode + return self._current_fan_mode.name @property def fan_list(self): @@ -315,36 +315,41 @@ def is_on(self) -> bool: @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set target temperature.""" + from miio.airconditioningcompanion import OperationMode if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) if kwargs.get(ATTR_OPERATION_MODE) is not None: - self._current_operation = kwargs.get(ATTR_OPERATION_MODE) + self._current_operation = OperationMode[ + kwargs.get(ATTR_OPERATION_MODE).title()] yield from self._send_configuration() @asyncio.coroutine def async_set_swing_mode(self, swing_mode): """Set target temperature.""" - self._current_swing_mode = swing_mode + from miio.airconditioningcompanion import SwingMode + self._current_swing_mode = SwingMode[swing_mode.title()] yield from self._send_configuration() @asyncio.coroutine def async_set_fan_mode(self, fan): """Set the fan mode.""" - self._current_fan_mode = fan + from miio.airconditioningcompanion import FanSpeed + self._current_fan_mode = FanSpeed[fan.title()] yield from self._send_configuration() @asyncio.coroutine def async_set_operation_mode(self, operation_mode): """Set operation mode.""" - self._current_operation = operation_mode + from miio.airconditioningcompanion import OperationMode + self._current_operation = OperationMode[operation_mode.title()] yield from self._send_configuration() @property def current_swing_mode(self): """Return the current swing setting.""" - return self._current_swing_mode + return self._current_swing_mode.name @property def swing_list(self): @@ -355,7 +360,7 @@ def swing_list(self): @asyncio.coroutine def _send_configuration(self): from miio.airconditioningcompanion import \ - Power, OperationMode, FanSpeed, SwingMode, Led + Power, FanSpeed, SwingMode, Led if self._air_condition_model is not None: yield from self._try_command( @@ -363,10 +368,10 @@ def _send_configuration(self): self._device.send_configuration, self._air_condition_model, Power(int(self._state)), - OperationMode[self._current_operation], + self._current_operation, int(self._target_temperature), - FanSpeed[self._current_fan_mode], - SwingMode[self._current_swing_mode], + self._current_fan_mode, + self._current_swing_mode, Led.Off, ) else: From ae8e2e8224398aa2c43fe7fc1a0ee0d58aa178c6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 21 Aug 2018 16:37:16 +0200 Subject: [PATCH 15/39] Add xiaomi_miio_learn_command and xiaomi_miio_send_command service (Closes: #5) --- custom_components/climate/xiaomi_miio.py | 121 ++++++++++++++++++++--- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 17df420..6674243 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -14,13 +14,14 @@ from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, ATTR_OPERATION_MODE, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, ) + SUPPORT_SWING_MODE, DOMAIN, ) from homeassistant.const import ( TEMP_CELSIUS, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - CONF_NAME, CONF_HOST, CONF_TOKEN, ) + CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID, CONF_TIMEOUT, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -31,8 +32,12 @@ SUCCESS = ['ok'] DEFAULT_NAME = 'Xiaomi AC Companion' +DATA_KEY = 'climate.xiaomi_miio' TARGET_TEMPERATURE_STEP = 1 +DEFAULT_TIMEOUT = 10 +DEFAULT_SLOT = 1 + ATTR_AIR_CONDITION_MODEL = 'ac_model' ATTR_SWING_MODE = 'swing_mode' ATTR_FAN_SPEED = 'fan_speed' @@ -48,6 +53,8 @@ CONF_SENSOR = 'target_sensor' CONF_MIN_TEMP = 'min_temp' CONF_MAX_TEMP = 'max_temp' +CONF_SLOT = 'slot' +CONF_COMMAND = 'command' SCAN_INTERVAL = timedelta(seconds=15) @@ -60,12 +67,39 @@ vol.Optional(CONF_MAX_TEMP, default=30): vol.Coerce(int), }) +SERVICE_LEARN_COMMAND = 'xiaomi_miio_learn_command' +SERVICE_SEND_COMMAND = 'xiaomi_miio_send_command' + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SCHEMA_LEARN_COMMAND = SERVICE_SCHEMA.extend({ + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): + vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): + vol.All(int, vol.Range(min=1, max=1000000)), +}) + +SERVICE_SCHEMA_SEND_COMMAND = SERVICE_SCHEMA.extend({ + vol.Optional(CONF_COMMAND): cv.string, +}) + +SERVICE_TO_METHOD = { + SERVICE_LEARN_COMMAND: {'method': 'async_learn_command', + 'schema': SERVICE_SCHEMA_LEARN_COMMAND}, + SERVICE_SEND_COMMAND: {'method': 'async_send_command', + 'schema': SERVICE_SCHEMA_SEND_COMMAND}, +} + # pylint: disable=unused-argument @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the air conditioning companion from config.""" from miio import AirConditioningCompanion, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -89,9 +123,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error("Device unavailable or token incorrect: %s", ex) raise PlatformNotReady - async_add_devices([XiaomiAirConditioningCompanion( - hass, name, device, unique_id, sensor_entity_id, min_temp, max_temp)], - update_before_add=True) + air_conditioning_companion = XiaomiAirConditioningCompanion( + hass, name, device, unique_id, sensor_entity_id, min_temp, max_temp) + hass.data[DATA_KEY][host] = air_conditioning_companion + async_add_devices([air_conditioning_companion], update_before_add=True) + + async def async_service_handler(service): + """Map services to methods on XiaomiAirConditioningCompanion.""" + method = SERVICE_TO_METHOD.get(service.service) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [device for device in hass.data[DATA_KEY].values() if + device.entity_id in entity_ids] + else: + devices = hass.data[DATA_KEY].values() + + update_tasks = [] + for device in devices: + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) + update_tasks.append(device.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks, loop=hass.loop) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service].get('schema', SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema) class XiaomiAirConditioningCompanion(ClimateDevice): @@ -378,13 +440,50 @@ def _send_configuration(self): _LOGGER.error('Model number of the air condition unknown. ' 'Configuration cannot be sent.') - def _send_custom_command(self, command: str): - if command[0:2] == "01": + @asyncio.coroutine + def async_learn_command(self, slot, timeout): + """Learn a infrared command.""" + yield from self.hass.async_add_job(self._device.learn, slot) + + _LOGGER.info("Press the key you want Home Assistant to learn") + start_time = utcnow() + while (utcnow() - start_time) < timedelta(seconds=timeout): + message = yield from self.hass.async_add_job( + self._device.learn_result) + # FIXME: Improve python-miio here? + message = message[0] + _LOGGER.debug("Message received from device: '%s'", message) + if message.startswith('FE'): + log_msg = "Received command is: {}".format(message) + _LOGGER.info(log_msg) + self.hass.components.persistent_notification.async_create( + log_msg, title='Xiaomi Miio Remote') + yield from self.hass.async_add_job(self._device.learn_stop, slot) + return + + yield from asyncio.sleep(1, loop=self.hass.loop) + + yield from self.hass.async_add_job(self._device.learn_stop, slot) + _LOGGER.error("Timeout. No infrared command captured") + self.hass.components.persistent_notification.async_create( + "Timeout. No infrared command captured", + title='Xiaomi Miio Remote') + + @asyncio.coroutine + def async_send_command(self, command): + """Send a infrared command.""" + if command.startswith('01'): yield from self._try_command( "Sending new air conditioner configuration failed.", self._device.send_command, command) + elif command.startswith('FE'): + if self._air_condition_model is not None: + # Learned infrared commands has the prefix 'FE' + yield from self._try_command( + "Sending custom infrared command failed.", + self._device.send_ir_code, self._air_condition_model, command) + else: + _LOGGER.error('Model number of the air condition unknown. ' + 'IR command cannot be sent.') else: - # Learned infrared commands has the prefix 'FE' - yield from self._try_command( - "Sending new air conditioner configuration failed.", - self._device.send_ir_code, command) + _LOGGER.error('Invalid IR command.') From e3d05012c49f5a8bd1557ba1d1aafe74865c3e6f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 21 Aug 2018 16:42:40 +0200 Subject: [PATCH 16/39] Add infrared services to the README --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index b37c897..481a7d7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all * Fan Speed (Low, Medium, High, Auto) * Swing Mode (On, Off) * Target Temperature +* Capture and replay infrared commands * Attributes - ac_model - ac_power (on, off) @@ -39,3 +40,24 @@ climate: target_sensor: sensor.temperature_158d0001f53706 scan_interval: 60 ``` + +## Platform services + +#### Service `climate.xiaomi_miio_learn_command` + +Capture an infrared command. + +| Service data attribute | Optional | Description | +|---------------------------|----------|----------------------------------------------------------------------| +| `entity_id` | yes | Only act on a specific air purifier. Else targets all. | +| `slot` | yes | Storage slot. Defaults to slot ID 1. | +| `timeout` | yes | Capturing timeout. Defaults to 10 seconds. | + +#### Service `climate.xiaomi_miio_send_command` + +Send captured infrared command or device configuration. + +| Service data attribute | Optional | Description | +|---------------------------|----------|----------------------------------------------------------------------| +| `entity_id` | yes | Only act on a specific air purifier. Else targets all. | +| `command` | no | Infrared command. Must start with `FE` or `01`. | From 1aa10785d667aa67ec7abeba7df2a5e124803bab Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 23 Aug 2018 09:16:18 +0200 Subject: [PATCH 17/39] Update docstring --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 6674243..67fece8 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -389,7 +389,7 @@ def async_set_temperature(self, **kwargs): @asyncio.coroutine def async_set_swing_mode(self, swing_mode): - """Set target temperature.""" + """Set the swing mode.""" from miio.airconditioningcompanion import SwingMode self._current_swing_mode = SwingMode[swing_mode.title()] yield from self._send_configuration() From d45692fbb9592449c824caf0e72270d19a3bb502 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 21 Nov 2018 15:01:57 +0100 Subject: [PATCH 18/39] Use a better storage slot (Closes: #38) --- custom_components/climate/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 67fece8..ef92a07 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -36,7 +36,7 @@ TARGET_TEMPERATURE_STEP = 1 DEFAULT_TIMEOUT = 10 -DEFAULT_SLOT = 1 +DEFAULT_SLOT = 30 ATTR_AIR_CONDITION_MODEL = 'ac_model' ATTR_SWING_MODE = 'swing_mode' From 37168d1dffeba179f0cd966de56de10a4a27a6e4 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 1 Dec 2018 08:11:14 +0100 Subject: [PATCH 19/39] Add debugging paragraph --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 481a7d7..c99b6d5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,20 @@ climate: scan_interval: 60 ``` +## Debugging + +If the custom component doesn't work out of the box for your device please update your configuration to enable a higher log level: + +```yaml +# configuration.yaml + +logger: + default: warn + logs: + custom_components.climate.xiaomi_miio: debug + miio: debug +``` + ## Platform services #### Service `climate.xiaomi_miio_learn_command` From 980882bcfc509c55355218ee9f5abbfe51199740 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 1 Dec 2018 08:13:34 +0100 Subject: [PATCH 20/39] Update default slot and the lower limit of valid slots --- README.md | 2 +- custom_components/climate/xiaomi_miio.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c99b6d5..6844155 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Capture an infrared command. | Service data attribute | Optional | Description | |---------------------------|----------|----------------------------------------------------------------------| | `entity_id` | yes | Only act on a specific air purifier. Else targets all. | -| `slot` | yes | Storage slot. Defaults to slot ID 1. | +| `slot` | yes | Storage slot. Defaults to slot ID 30. | | `timeout` | yes | Capturing timeout. Defaults to 10 seconds. | #### Service `climate.xiaomi_miio_send_command` diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index ef92a07..198f454 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -78,7 +78,7 @@ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.All(int, vol.Range(min=0)), vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): - vol.All(int, vol.Range(min=1, max=1000000)), + vol.All(int, vol.Range(min=2, max=1000000)), }) SERVICE_SCHEMA_SEND_COMMAND = SERVICE_SCHEMA.extend({ From 5d1e65d54900c317647c50703464ee78214a71ba Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 2 Dec 2018 14:03:53 +0100 Subject: [PATCH 21/39] Fix unexpected keyword argument 'fan_mode' (Closes: #41) --- custom_components/climate/xiaomi_miio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 198f454..c70339c 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -395,10 +395,10 @@ def async_set_swing_mode(self, swing_mode): yield from self._send_configuration() @asyncio.coroutine - def async_set_fan_mode(self, fan): + def async_set_fan_mode(self, fan_mode): """Set the fan mode.""" from miio.airconditioningcompanion import FanSpeed - self._current_fan_mode = FanSpeed[fan.title()] + self._current_fan_mode = FanSpeed[fan_mode.title()] yield from self._send_configuration() @asyncio.coroutine From 0a4ee2eb3f0e2734368b599705ebda150f079436 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 20 Dec 2018 07:44:14 +0100 Subject: [PATCH 22/39] Provide operation modes in lower-case to improve alexa / google support --- custom_components/climate/xiaomi_miio.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index c70339c..6cb32e0 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -256,7 +256,6 @@ def async_turn_off(self, **kwargs) -> None: def async_update(self): """Update the state of this climate device.""" from miio import DeviceException - from miio.airconditioningcompanion import SwingMode try: state = yield from self.hass.async_add_job(self._device.status) @@ -350,24 +349,24 @@ def target_temperature(self): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation.name + return self._current_operation.name.lower() @property def operation_list(self): """Return the list of available operation modes.""" from miio.airconditioningcompanion import OperationMode - return [mode.name for mode in OperationMode] + return [mode.name.lower() for mode in OperationMode] @property def current_fan_mode(self): """Return the current fan mode.""" - return self._current_fan_mode.name + return self._current_fan_mode.name.lower() @property def fan_list(self): """Return the list of available fan modes.""" from miio.airconditioningcompanion import FanSpeed - return [speed.name for speed in FanSpeed] + return [speed.name.lower() for speed in FanSpeed] @property def is_on(self) -> bool: @@ -411,18 +410,18 @@ def async_set_operation_mode(self, operation_mode): @property def current_swing_mode(self): """Return the current swing setting.""" - return self._current_swing_mode.name + return self._current_swing_mode.name.lower() @property def swing_list(self): """List of available swing modes.""" from miio.airconditioningcompanion import SwingMode - return [mode.name for mode in SwingMode] + return [mode.name.lower() for mode in SwingMode] @asyncio.coroutine def _send_configuration(self): from miio.airconditioningcompanion import \ - Power, FanSpeed, SwingMode, Led + Power, Led if self._air_condition_model is not None: yield from self._try_command( From 4c35a728cfb7f27a14980a162a245152e49ccb93 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 28 Dec 2018 23:52:38 +0100 Subject: [PATCH 23/39] Fix current value of the climate card dropdowns --- custom_components/climate/xiaomi_miio.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 6cb32e0..4fd402e 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -40,7 +40,7 @@ ATTR_AIR_CONDITION_MODEL = 'ac_model' ATTR_SWING_MODE = 'swing_mode' -ATTR_FAN_SPEED = 'fan_speed' +ATTR_FAN_MODE = 'fan_mode' ATTR_LOAD_POWER = 'load_power' ATTR_LED = 'led' @@ -176,7 +176,6 @@ def __init__(self, hass, name, device, unique_id, sensor_entity_id, ATTR_LOAD_POWER: None, ATTR_TEMPERATURE: None, ATTR_SWING_MODE: None, - ATTR_FAN_SPEED: None, ATTR_OPERATION_MODE: None, ATTR_LED: None, } @@ -267,9 +266,9 @@ def async_update(self): ATTR_AIR_CONDITION_MODEL: state.air_condition_model.hex(), ATTR_LOAD_POWER: state.load_power, ATTR_TEMPERATURE: state.target_temperature, - ATTR_SWING_MODE: state.swing_mode.name, - ATTR_FAN_SPEED: state.fan_speed.name, - ATTR_OPERATION_MODE: state.mode.name, + ATTR_SWING_MODE: state.swing_mode.name.lower(), + ATTR_FAN_MODE: state.fan_speed.name.lower(), + ATTR_OPERATION_MODE: state.mode.name.lower(), ATTR_LED: state.led, }) From 4057cb7846067b44ac2615a1c0a847594859d245 Mon Sep 17 00:00:00 2001 From: sekkr1 Date: Mon, 31 Dec 2018 09:59:19 +0200 Subject: [PATCH 24/39] Added off as an operation mode to comply with g home and lovelace (#46) --- custom_components/climate/xiaomi_miio.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/climate/xiaomi_miio.py index 4fd402e..6c5f7e3 100644 --- a/custom_components/climate/xiaomi_miio.py +++ b/custom_components/climate/xiaomi_miio.py @@ -268,7 +268,7 @@ def async_update(self): ATTR_TEMPERATURE: state.target_temperature, ATTR_SWING_MODE: state.swing_mode.name.lower(), ATTR_FAN_MODE: state.fan_speed.name.lower(), - ATTR_OPERATION_MODE: state.mode.name.lower(), + ATTR_OPERATION_MODE: state.mode.name.lower() if self._state else "off", ATTR_LED: state.led, }) @@ -348,13 +348,13 @@ def target_temperature(self): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation.name.lower() + return self._current_operation.name.lower() if self._state else "off" @property def operation_list(self): """Return the list of available operation modes.""" from miio.airconditioningcompanion import OperationMode - return [mode.name.lower() for mode in OperationMode] + return [mode.name.lower() for mode in OperationMode] + ["off"] @property def current_fan_mode(self): @@ -402,8 +402,12 @@ def async_set_fan_mode(self, fan_mode): @asyncio.coroutine def async_set_operation_mode(self, operation_mode): """Set operation mode.""" - from miio.airconditioningcompanion import OperationMode - self._current_operation = OperationMode[operation_mode.title()] + if operation_mode == "off": + self._state = False + else: + from miio.airconditioningcompanion import OperationMode + self._current_operation = OperationMode[operation_mode.title()] + self._state = True yield from self._send_configuration() @property From c4c181d2f7cb62966cc34e6452278aa0ac3fd656 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 31 Dec 2018 17:43:12 +0100 Subject: [PATCH 25/39] Add climate entity screenshot --- climate.png | Bin 0 -> 15974 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 climate.png diff --git a/climate.png b/climate.png new file mode 100644 index 0000000000000000000000000000000000000000..0f9c8b33b77434ccab6a7b72dd54de8fd5fb6505 GIT binary patch literal 15974 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�rz2T?X=AZNKNNQoR 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 Date: Mon, 31 Dec 2018 17:49:07 +0100 Subject: [PATCH 26/39] Add screenshot to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6844155..4e93b81 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ climate: scan_interval: 60 ``` +![climate entity](climate.png "climate entity") + ## Debugging If the custom component doesn't work out of the box for your device please update your configuration to enable a higher log level: From 65adea80a838f039286b51c00da31bebcaa400d6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 25 Feb 2019 17:54:08 +0100 Subject: [PATCH 27/39] Apply new file structure of HA 0.88 --- .../{climate/xiaomi_miio.py => xiaomi_miio/climate.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename custom_components/{climate/xiaomi_miio.py => xiaomi_miio/climate.py} (100%) diff --git a/custom_components/climate/xiaomi_miio.py b/custom_components/xiaomi_miio/climate.py similarity index 100% rename from custom_components/climate/xiaomi_miio.py rename to custom_components/xiaomi_miio/climate.py From 210368f98a6f0c107a3ea32c8817f84f735a404e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 25 Feb 2019 18:06:34 +0100 Subject: [PATCH 28/39] Add required __init__.py --- custom_components/xiaomi_miio/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 custom_components/xiaomi_miio/__init__.py diff --git a/custom_components/xiaomi_miio/__init__.py b/custom_components/xiaomi_miio/__init__.py new file mode 100644 index 0000000..e69de29 From 390faa97da5a373efc2618799c29556ef5cb8ae3 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 7 Mar 2019 07:36:32 +0100 Subject: [PATCH 29/39] Add HA 0.89 compatibility (Closes: #49) --- custom_components/xiaomi_miio/climate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/custom_components/xiaomi_miio/climate.py b/custom_components/xiaomi_miio/climate.py index 6c5f7e3..307d2d3 100644 --- a/custom_components/xiaomi_miio/climate.py +++ b/custom_components/xiaomi_miio/climate.py @@ -12,12 +12,13 @@ from homeassistant.core import callback from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, ATTR_OPERATION_MODE, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, DOMAIN, ) + ClimateDevice, PLATFORM_SCHEMA, ) +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, DOMAIN, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, + SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - TEMP_CELSIUS, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID, CONF_TIMEOUT, ) + ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, + CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv From 106abb6f3517fcc02a85d3816dbe6854c298e4b1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 7 Mar 2019 10:20:04 +0100 Subject: [PATCH 30/39] Rename platform to avoid conflicts with the official platform xiaomi_miio (Closes: #50) --- README.md | 4 ++-- .../__init__.py | 0 .../climate.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename custom_components/{xiaomi_miio => xiaomi_miio_airconditioningcompanion}/__init__.py (100%) rename custom_components/{xiaomi_miio => xiaomi_miio_airconditioningcompanion}/climate.py (100%) diff --git a/README.md b/README.md index 4e93b81..af8d39d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all # configuration.yaml climate: - - platform: xiaomi_miio + - platform: xiaomi_miio_airconditioningcompanion name: Aqara Air Conditioning Companion host: 192.168.130.71 token: b7c4a758c251955d2c24b1d9e41ce47d @@ -53,7 +53,7 @@ If the custom component doesn't work out of the box for your device please updat logger: default: warn logs: - custom_components.climate.xiaomi_miio: debug + custom_components.xiaomi_miio_airconditioningcompanion.climate: debug miio: debug ``` diff --git a/custom_components/xiaomi_miio/__init__.py b/custom_components/xiaomi_miio_airconditioningcompanion/__init__.py similarity index 100% rename from custom_components/xiaomi_miio/__init__.py rename to custom_components/xiaomi_miio_airconditioningcompanion/__init__.py diff --git a/custom_components/xiaomi_miio/climate.py b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py similarity index 100% rename from custom_components/xiaomi_miio/climate.py rename to custom_components/xiaomi_miio_airconditioningcompanion/climate.py From 9e82c1c15357b3ce7e1fd54aee36f037577510a8 Mon Sep 17 00:00:00 2001 From: natic Date: Fri, 26 Apr 2019 15:31:37 +0800 Subject: [PATCH 31/39] Add mainfest.json for HA 0.92 (#59) --- .../xiaomi_miio_airconditioningcompanion/manifest.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 custom_components/xiaomi_miio_airconditioningcompanion/manifest.json diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json new file mode 100644 index 0000000..9147525 --- /dev/null +++ b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "xiaomi_miio_airconditioningcompanion", + "name": "AC Companion", + "documentation": "", + "dependencies": ["sensor"], + "codeowners": [], + "requirements": ["python-miio>=0.4.0"] +} \ No newline at end of file From c81a4ef37d2d641765b01bad44ad7a336874a45b Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 26 Apr 2019 20:57:59 +0200 Subject: [PATCH 32/39] Add HA 0.92 compat --- .../xiaomi_miio_airconditioningcompanion/climate.py | 4 ---- .../manifest.json | 13 +++++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py index 307d2d3..93a2646 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py +++ b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py @@ -26,10 +26,6 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-miio>=0.4.0'] - -DEPENDENCIES = ['sensor'] - SUCCESS = ['ok'] DEFAULT_NAME = 'Xiaomi AC Companion' diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json index 9147525..4bdfe8c 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json +++ b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json @@ -1,8 +1,13 @@ { "domain": "xiaomi_miio_airconditioningcompanion", - "name": "AC Companion", - "documentation": "", + "name": "Xiaomi Mi and Aqara Air Conditioning Companion", + "documentation": "https://github.com/syssi/xiaomi_airconditioningcompanion", + "requirements": [ + "construct==2.9.45", + "python-miio==0.4.5" + ], "dependencies": ["sensor"], - "codeowners": [], - "requirements": ["python-miio>=0.4.0"] + "codeowners": [ + "@syssi" + ] } \ No newline at end of file From 79397544ce07a5e5cffd666494e90c865c4f3f77 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 2 Jun 2019 20:50:32 +0200 Subject: [PATCH 33/39] Use well known states to be google home & amazon alexa complaint (Closes: #37) --- .../climate.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py index 93a2646..d0ae7fa 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py +++ b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/climate.xiaomi_miio """ +import enum import logging import asyncio from functools import partial @@ -153,6 +154,15 @@ async def async_service_handler(service): DOMAIN, service, async_service_handler, schema=schema) +class OperationMode(enum.Enum): + Heat = 'heat' + Cool = 'cool' + Auto = 'auto' + Dehumidify = 'dry' + Ventilate = 'fan_only' + Off = 'off' + + class XiaomiAirConditioningCompanion(ClimateDevice): """Representation of a Xiaomi Air Conditioning Companion.""" @@ -269,7 +279,7 @@ def async_update(self): ATTR_LED: state.led, }) - self._current_operation = state.mode + self._current_operation = OperationMode[state.mode.name].value self._target_temperature = state.target_temperature self._current_fan_mode = state.fan_speed @@ -345,13 +355,12 @@ def target_temperature(self): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation.name.lower() if self._state else "off" + return self._current_operation @property def operation_list(self): """Return the list of available operation modes.""" - from miio.airconditioningcompanion import OperationMode - return [mode.name.lower() for mode in OperationMode] + ["off"] + return [mode.value for mode in OperationMode] @property def current_fan_mode(self): @@ -372,13 +381,11 @@ def is_on(self) -> bool: @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set target temperature.""" - from miio.airconditioningcompanion import OperationMode if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) if kwargs.get(ATTR_OPERATION_MODE) is not None: - self._current_operation = OperationMode[ - kwargs.get(ATTR_OPERATION_MODE).title()] + self._current_operation = OperationMode(kwargs.get(ATTR_OPERATION_MODE)) yield from self._send_configuration() @@ -399,11 +406,10 @@ def async_set_fan_mode(self, fan_mode): @asyncio.coroutine def async_set_operation_mode(self, operation_mode): """Set operation mode.""" - if operation_mode == "off": + if operation_mode == OperationMode.Off.value: self._state = False else: - from miio.airconditioningcompanion import OperationMode - self._current_operation = OperationMode[operation_mode.title()] + self._current_operation = OperationMode(operation_mode).value self._state = True yield from self._send_configuration() @@ -421,7 +427,7 @@ def swing_list(self): @asyncio.coroutine def _send_configuration(self): from miio.airconditioningcompanion import \ - Power, Led + Power, Led, OperationMode as MiioOperationMode if self._air_condition_model is not None: yield from self._try_command( @@ -429,7 +435,7 @@ def _send_configuration(self): self._device.send_configuration, self._air_condition_model, Power(int(self._state)), - self._current_operation, + MiioOperationMode[OperationMode(self._current_operation).name], int(self._target_temperature), self._current_fan_mode, self._current_swing_mode, From 9fa5223e4a2a1fa889ae667c9f9c5e6c15312bf3 Mon Sep 17 00:00:00 2001 From: Bruce Dun Date: Fri, 2 Aug 2019 14:28:51 +0800 Subject: [PATCH 34/39] Add HA 0.96 compatibility for new Climate 1.0 (#69) --- .../climate.py | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py index d0ae7fa..7f6b22b 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py +++ b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py @@ -15,8 +15,9 @@ from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, ) from homeassistant.components.climate.const import ( - ATTR_OPERATION_MODE, DOMAIN, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) + ATTR_HVAC_MODE, DOMAIN, HVAC_MODES, HVAC_MODE_OFF, HVAC_MODE_HEAT, + HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, TEMP_CELSIUS, ) @@ -42,10 +43,8 @@ ATTR_LOAD_POWER = 'load_power' ATTR_LED = 'led' -SUPPORT_FLAGS = (SUPPORT_ON_OFF | - SUPPORT_TARGET_TEMPERATURE | +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE | SUPPORT_SWING_MODE) CONF_SENSOR = 'target_sensor' @@ -155,12 +154,12 @@ async def async_service_handler(service): class OperationMode(enum.Enum): - Heat = 'heat' - Cool = 'cool' - Auto = 'auto' - Dehumidify = 'dry' - Ventilate = 'fan_only' - Off = 'off' + Heat = HVAC_MODE_HEAT + Cool = HVAC_MODE_COOL + Auto = HVAC_MODE_AUTO + Dehumidify = HVAC_MODE_DRY + Ventilate = HVAC_MODE_FAN_ONLY + Off = HVAC_MODE_OFF class XiaomiAirConditioningCompanion(ClimateDevice): @@ -183,16 +182,17 @@ def __init__(self, hass, name, device, unique_id, sensor_entity_id, ATTR_LOAD_POWER: None, ATTR_TEMPERATURE: None, ATTR_SWING_MODE: None, - ATTR_OPERATION_MODE: None, + ATTR_HVAC_MODE: None, ATTR_LED: None, } self._max_temp = max_temp self._min_temp = min_temp self._current_temperature = None - self._current_swing_mode = None - self._current_operation = None - self._current_fan_mode = None + self._swing_mode = None + self._last_on_operation = None + self._hvac_mode = None + self._fan_mode = None self._air_condition_model = None self._target_temperature = None @@ -268,23 +268,25 @@ def async_update(self): _LOGGER.debug("Got new state: %s", state) self._available = True - self._state = state.is_on self._state_attrs.update({ ATTR_AIR_CONDITION_MODEL: state.air_condition_model.hex(), ATTR_LOAD_POWER: state.load_power, ATTR_TEMPERATURE: state.target_temperature, ATTR_SWING_MODE: state.swing_mode.name.lower(), ATTR_FAN_MODE: state.fan_speed.name.lower(), - ATTR_OPERATION_MODE: state.mode.name.lower() if self._state else "off", + ATTR_HVAC_MODE: state.mode.name.lower() if self._state else "off", ATTR_LED: state.led, }) - - self._current_operation = OperationMode[state.mode.name].value + self._last_on_operation = OperationMode[state.mode.name].value + if state.power == 'off': + self._hvac_mode = HVAC_MODE_OFF + self._state = False + else: + self._hvac_mode = self._last_on_operation + self._state = True self._target_temperature = state.target_temperature - - self._current_fan_mode = state.fan_speed - self._current_swing_mode = state.swing_mode - + self._fan_mode = state.fan_speed + self._swing_mode = state.swing_mode if self._air_condition_model is None: self._air_condition_model = state.air_condition_model.hex() @@ -353,39 +355,38 @@ def target_temperature(self): return self._target_temperature @property - def current_operation(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation + def hvac_mode(self): + """Return new hvac mode ie. heat, cool, fan only.""" + return self._hvac_mode + + @property + def last_on_operation(self): + """Return the last operation when the AC is on (ie heat, cool, fan only)""" + return self._last_on_operation @property - def operation_list(self): - """Return the list of available operation modes.""" + def hvac_modes(self): + """Return the list of available hvac modes.""" return [mode.value for mode in OperationMode] @property - def current_fan_mode(self): + def fan_mode(self): """Return the current fan mode.""" - return self._current_fan_mode.name.lower() + return self._fan_mode.name.lower() @property - def fan_list(self): + def fan_modes(self): """Return the list of available fan modes.""" from miio.airconditioningcompanion import FanSpeed return [speed.name.lower() for speed in FanSpeed] - @property - def is_on(self) -> bool: - """Return True if the entity is on.""" - return self._state - @asyncio.coroutine def async_set_temperature(self, **kwargs): """Set target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) - - if kwargs.get(ATTR_OPERATION_MODE) is not None: - self._current_operation = OperationMode(kwargs.get(ATTR_OPERATION_MODE)) + if kwargs.get(ATTR_HVAC_MODE) is not None: + self._hvac_mode = OperationMode(kwargs.get(ATTR_HVAC_MODE)) yield from self._send_configuration() @@ -393,33 +394,37 @@ def async_set_temperature(self, **kwargs): def async_set_swing_mode(self, swing_mode): """Set the swing mode.""" from miio.airconditioningcompanion import SwingMode - self._current_swing_mode = SwingMode[swing_mode.title()] + self._swing_mode = SwingMode[swing_mode.title()] yield from self._send_configuration() @asyncio.coroutine def async_set_fan_mode(self, fan_mode): """Set the fan mode.""" from miio.airconditioningcompanion import FanSpeed - self._current_fan_mode = FanSpeed[fan_mode.title()] + self._fan_mode = FanSpeed[fan_mode.title()] yield from self._send_configuration() @asyncio.coroutine - def async_set_operation_mode(self, operation_mode): - """Set operation mode.""" - if operation_mode == OperationMode.Off.value: - self._state = False + def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + if hvac_mode == OperationMode.Off.value: + result = yield from self._try_command( + "Turning the miio device off failed.", self._device.off) + if result: + self._state = False + self._hvac_mode = HVAC_MODE_OFF else: - self._current_operation = OperationMode(operation_mode).value + self._hvac_mode = OperationMode(hvac_mode).value self._state = True - yield from self._send_configuration() + yield from self._send_configuration() @property - def current_swing_mode(self): + def swing_mode(self): """Return the current swing setting.""" - return self._current_swing_mode.name.lower() + return self._swing_mode.name.lower() @property - def swing_list(self): + def swing_modes(self): """List of available swing modes.""" from miio.airconditioningcompanion import SwingMode return [mode.name.lower() for mode in SwingMode] @@ -428,17 +433,16 @@ def swing_list(self): def _send_configuration(self): from miio.airconditioningcompanion import \ Power, Led, OperationMode as MiioOperationMode - if self._air_condition_model is not None: yield from self._try_command( "Sending new air conditioner configuration failed.", self._device.send_configuration, self._air_condition_model, Power(int(self._state)), - MiioOperationMode[OperationMode(self._current_operation).name], + MiioOperationMode[OperationMode(self._hvac_mode).name] if self._state else MiioOperationMode[OperationMode(self._last_on_operation).name], int(self._target_temperature), - self._current_fan_mode, - self._current_swing_mode, + self._fan_mode, + self._swing_mode, Led.Off, ) else: From 5dc0aa99b0454934f08a8daaee99fff4454e160e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Oct 2019 21:30:47 +0200 Subject: [PATCH 35/39] Bump python-miio version to 0.4.6 --- .../xiaomi_miio_airconditioningcompanion/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json index 4bdfe8c..ecb3e83 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json +++ b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://github.com/syssi/xiaomi_airconditioningcompanion", "requirements": [ "construct==2.9.45", - "python-miio==0.4.5" + "python-miio==0.4.6" ], "dependencies": ["sensor"], "codeowners": [ From 7db0428f821e4ccfba6fa47c96b40d80e2d385a9 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 12 Oct 2019 09:09:35 +0200 Subject: [PATCH 36/39] Blackify code --- .flake8.ini | 2 + .pre-commit-config.yaml | 6 + .../climate.py | 268 +++++++++++------- 3 files changed, 170 insertions(+), 106 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.flake8.ini b/.flake8.ini index 4fe126b..ed40209 100644 --- a/.flake8.ini +++ b/.flake8.ini @@ -1,3 +1,5 @@ [flake8] exclude = .git,.tox,__pycache__ max-line-length = 100 +select = C,E,F,W,B,B950 +ignore = E501,W503,E203 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..da90577 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + language_version: python3 diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py index 7f6b22b..484e9c8 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/climate.py +++ b/custom_components/xiaomi_miio_airconditioningcompanion/climate.py @@ -12,15 +12,31 @@ import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, ) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, DOMAIN, HVAC_MODES, HVAC_MODE_OFF, HVAC_MODE_HEAT, - HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) + ATTR_HVAC_MODE, + DOMAIN, + HVAC_MODES, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_AUTO, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + SUPPORT_SWING_MODE, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, - CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, TEMP_CELSIUS, ) + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_HOST, + CONF_TOKEN, + CONF_TIMEOUT, + TEMP_CELSIUS, +) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv @@ -28,65 +44,71 @@ _LOGGER = logging.getLogger(__name__) -SUCCESS = ['ok'] +SUCCESS = ["ok"] -DEFAULT_NAME = 'Xiaomi AC Companion' -DATA_KEY = 'climate.xiaomi_miio' +DEFAULT_NAME = "Xiaomi AC Companion" +DATA_KEY = "climate.xiaomi_miio" TARGET_TEMPERATURE_STEP = 1 DEFAULT_TIMEOUT = 10 DEFAULT_SLOT = 30 -ATTR_AIR_CONDITION_MODEL = 'ac_model' -ATTR_SWING_MODE = 'swing_mode' -ATTR_FAN_MODE = 'fan_mode' -ATTR_LOAD_POWER = 'load_power' -ATTR_LED = 'led' +ATTR_AIR_CONDITION_MODEL = "ac_model" +ATTR_SWING_MODE = "swing_mode" +ATTR_FAN_MODE = "fan_mode" +ATTR_LOAD_POWER = "load_power" +ATTR_LED = "led" -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | - SUPPORT_FAN_MODE | - SUPPORT_SWING_MODE) +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE -CONF_SENSOR = 'target_sensor' -CONF_MIN_TEMP = 'min_temp' -CONF_MAX_TEMP = 'max_temp' -CONF_SLOT = 'slot' -CONF_COMMAND = 'command' +CONF_SENSOR = "target_sensor" +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" +CONF_SLOT = "slot" +CONF_COMMAND = "command" SCAN_INTERVAL = timedelta(seconds=15) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), - vol.Required(CONF_SENSOR): cv.entity_id, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MIN_TEMP, default=16): vol.Coerce(int), - vol.Optional(CONF_MAX_TEMP, default=30): vol.Coerce(int), -}) - -SERVICE_LEARN_COMMAND = 'xiaomi_miio_learn_command' -SERVICE_SEND_COMMAND = 'xiaomi_miio_send_command' - -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, -}) - -SERVICE_SCHEMA_LEARN_COMMAND = SERVICE_SCHEMA.extend({ - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): - vol.All(int, vol.Range(min=0)), - vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): - vol.All(int, vol.Range(min=2, max=1000000)), -}) - -SERVICE_SCHEMA_SEND_COMMAND = SERVICE_SCHEMA.extend({ - vol.Optional(CONF_COMMAND): cv.string, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Required(CONF_SENSOR): cv.entity_id, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MIN_TEMP, default=16): vol.Coerce(int), + vol.Optional(CONF_MAX_TEMP, default=30): vol.Coerce(int), + } +) + +SERVICE_LEARN_COMMAND = "xiaomi_miio_learn_command" +SERVICE_SEND_COMMAND = "xiaomi_miio_send_command" + +SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) + +SERVICE_SCHEMA_LEARN_COMMAND = SERVICE_SCHEMA.extend( + { + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.All( + int, vol.Range(min=0) + ), + vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): vol.All( + int, vol.Range(min=2, max=1000000) + ), + } +) + +SERVICE_SCHEMA_SEND_COMMAND = SERVICE_SCHEMA.extend( + {vol.Optional(CONF_COMMAND): cv.string} +) SERVICE_TO_METHOD = { - SERVICE_LEARN_COMMAND: {'method': 'async_learn_command', - 'schema': SERVICE_SCHEMA_LEARN_COMMAND}, - SERVICE_SEND_COMMAND: {'method': 'async_send_command', - 'schema': SERVICE_SCHEMA_SEND_COMMAND}, + SERVICE_LEARN_COMMAND: { + "method": "async_learn_command", + "schema": SERVICE_SCHEMA_LEARN_COMMAND, + }, + SERVICE_SEND_COMMAND: { + "method": "async_send_command", + "schema": SERVICE_SCHEMA_SEND_COMMAND, + }, } @@ -95,6 +117,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the air conditioning companion from config.""" from miio import AirConditioningCompanion, DeviceException + if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -112,45 +135,53 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device_info = device.info() model = device_info.model unique_id = "{}-{}".format(model, device_info.mac_address) - _LOGGER.info("%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version) + _LOGGER.info( + "%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version, + ) except DeviceException as ex: _LOGGER.error("Device unavailable or token incorrect: %s", ex) raise PlatformNotReady air_conditioning_companion = XiaomiAirConditioningCompanion( - hass, name, device, unique_id, sensor_entity_id, min_temp, max_temp) + hass, name, device, unique_id, sensor_entity_id, min_temp, max_temp + ) hass.data[DATA_KEY][host] = air_conditioning_companion async_add_devices([air_conditioning_companion], update_before_add=True) async def async_service_handler(service): """Map services to methods on XiaomiAirConditioningCompanion.""" method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items() - if key != ATTR_ENTITY_ID} + params = { + key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID + } entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - devices = [device for device in hass.data[DATA_KEY].values() if - device.entity_id in entity_ids] + devices = [ + device + for device in hass.data[DATA_KEY].values() + if device.entity_id in entity_ids + ] else: devices = hass.data[DATA_KEY].values() update_tasks = [] for device in devices: - if not hasattr(device, method['method']): + if not hasattr(device, method["method"]): continue - await getattr(device, method['method'])(**params) + await getattr(device, method["method"])(**params) update_tasks.append(device.async_update_ha_state(True)) if update_tasks: await asyncio.wait(update_tasks, loop=hass.loop) for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service].get('schema', SERVICE_SCHEMA) + schema = SERVICE_TO_METHOD[service].get("schema", SERVICE_SCHEMA) hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema) + DOMAIN, service, async_service_handler, schema=schema + ) class OperationMode(enum.Enum): @@ -165,8 +196,9 @@ class OperationMode(enum.Enum): class XiaomiAirConditioningCompanion(ClimateDevice): """Representation of a Xiaomi Air Conditioning Companion.""" - def __init__(self, hass, name, device, unique_id, sensor_entity_id, - min_temp, max_temp): + def __init__( + self, hass, name, device, unique_id, sensor_entity_id, min_temp, max_temp + ): """Initialize the climate device.""" self.hass = hass @@ -197,8 +229,7 @@ def __init__(self, hass, name, device, unique_id, sensor_entity_id, self._target_temperature = None if sensor_entity_id: - async_track_state_change( - hass, sensor_entity_id, self._async_sensor_changed) + async_track_state_change(hass, sensor_entity_id, self._async_sensor_changed) sensor_state = hass.states.get(sensor_entity_id) if sensor_state: self._async_update_temp(sensor_state) @@ -206,16 +237,17 @@ def __init__(self, hass, name, device, unique_id, sensor_entity_id, @callback def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" - if state.state is None or state.state == 'unknown': + if state.state is None or state.state == "unknown": return unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: self._current_temperature = self.hass.config.units.temperature( - float(state.state), unit) + float(state.state), unit + ) except ValueError as ex: - _LOGGER.error('Unable to update from sensor: %s', ex) + _LOGGER.error("Unable to update from sensor: %s", ex) @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): @@ -228,9 +260,9 @@ def _async_sensor_changed(self, entity_id, old_state, new_state): def _try_command(self, mask_error, func, *args, **kwargs): """Call a AC companion command handling error messages.""" from miio import DeviceException + try: - result = yield from self.hass.async_add_job( - partial(func, *args, **kwargs)) + result = yield from self.hass.async_add_job(partial(func, *args, **kwargs)) _LOGGER.debug("Response received: %s", result) @@ -244,7 +276,8 @@ def _try_command(self, mask_error, func, *args, **kwargs): def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn the miio device on.""" result = yield from self._try_command( - "Turning the miio device on failed.", self._device.on) + "Turning the miio device on failed.", self._device.on + ) if result: self._state = True @@ -253,7 +286,8 @@ def async_turn_on(self, speed: str = None, **kwargs) -> None: def async_turn_off(self, **kwargs) -> None: """Turn the miio device off.""" result = yield from self._try_command( - "Turning the miio device off failed.", self._device.off) + "Turning the miio device off failed.", self._device.off + ) if result: self._state = False @@ -268,17 +302,19 @@ def async_update(self): _LOGGER.debug("Got new state: %s", state) self._available = True - self._state_attrs.update({ - ATTR_AIR_CONDITION_MODEL: state.air_condition_model.hex(), - ATTR_LOAD_POWER: state.load_power, - ATTR_TEMPERATURE: state.target_temperature, - ATTR_SWING_MODE: state.swing_mode.name.lower(), - ATTR_FAN_MODE: state.fan_speed.name.lower(), - ATTR_HVAC_MODE: state.mode.name.lower() if self._state else "off", - ATTR_LED: state.led, - }) + self._state_attrs.update( + { + ATTR_AIR_CONDITION_MODEL: state.air_condition_model.hex(), + ATTR_LOAD_POWER: state.load_power, + ATTR_TEMPERATURE: state.target_temperature, + ATTR_SWING_MODE: state.swing_mode.name.lower(), + ATTR_FAN_MODE: state.fan_speed.name.lower(), + ATTR_HVAC_MODE: state.mode.name.lower() if self._state else "off", + ATTR_LED: state.led, + } + ) self._last_on_operation = OperationMode[state.mode.name].value - if state.power == 'off': + if state.power == "off": self._hvac_mode = HVAC_MODE_OFF self._state = False else: @@ -378,6 +414,7 @@ def fan_mode(self): def fan_modes(self): """Return the list of available fan modes.""" from miio.airconditioningcompanion import FanSpeed + return [speed.name.lower() for speed in FanSpeed] @asyncio.coroutine @@ -394,6 +431,7 @@ def async_set_temperature(self, **kwargs): def async_set_swing_mode(self, swing_mode): """Set the swing mode.""" from miio.airconditioningcompanion import SwingMode + self._swing_mode = SwingMode[swing_mode.title()] yield from self._send_configuration() @@ -401,6 +439,7 @@ def async_set_swing_mode(self, swing_mode): def async_set_fan_mode(self, fan_mode): """Set the fan mode.""" from miio.airconditioningcompanion import FanSpeed + self._fan_mode = FanSpeed[fan_mode.title()] yield from self._send_configuration() @@ -409,7 +448,8 @@ def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == OperationMode.Off.value: result = yield from self._try_command( - "Turning the miio device off failed.", self._device.off) + "Turning the miio device off failed.", self._device.off + ) if result: self._state = False self._hvac_mode = HVAC_MODE_OFF @@ -427,27 +467,36 @@ def swing_mode(self): def swing_modes(self): """List of available swing modes.""" from miio.airconditioningcompanion import SwingMode + return [mode.name.lower() for mode in SwingMode] @asyncio.coroutine def _send_configuration(self): - from miio.airconditioningcompanion import \ - Power, Led, OperationMode as MiioOperationMode + from miio.airconditioningcompanion import ( + Power, + Led, + OperationMode as MiioOperationMode, + ) + if self._air_condition_model is not None: yield from self._try_command( "Sending new air conditioner configuration failed.", self._device.send_configuration, self._air_condition_model, Power(int(self._state)), - MiioOperationMode[OperationMode(self._hvac_mode).name] if self._state else MiioOperationMode[OperationMode(self._last_on_operation).name], + MiioOperationMode[OperationMode(self._hvac_mode).name] + if self._state + else MiioOperationMode[OperationMode(self._last_on_operation).name], int(self._target_temperature), self._fan_mode, self._swing_mode, Led.Off, ) else: - _LOGGER.error('Model number of the air condition unknown. ' - 'Configuration cannot be sent.') + _LOGGER.error( + "Model number of the air condition unknown. " + "Configuration cannot be sent." + ) @asyncio.coroutine def async_learn_command(self, slot, timeout): @@ -457,16 +506,16 @@ def async_learn_command(self, slot, timeout): _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): - message = yield from self.hass.async_add_job( - self._device.learn_result) + message = yield from self.hass.async_add_job(self._device.learn_result) # FIXME: Improve python-miio here? message = message[0] _LOGGER.debug("Message received from device: '%s'", message) - if message.startswith('FE'): + if message.startswith("FE"): log_msg = "Received command is: {}".format(message) _LOGGER.info(log_msg) self.hass.components.persistent_notification.async_create( - log_msg, title='Xiaomi Miio Remote') + log_msg, title="Xiaomi Miio Remote" + ) yield from self.hass.async_add_job(self._device.learn_stop, slot) return @@ -475,24 +524,31 @@ def async_learn_command(self, slot, timeout): yield from self.hass.async_add_job(self._device.learn_stop, slot) _LOGGER.error("Timeout. No infrared command captured") self.hass.components.persistent_notification.async_create( - "Timeout. No infrared command captured", - title='Xiaomi Miio Remote') + "Timeout. No infrared command captured", title="Xiaomi Miio Remote" + ) @asyncio.coroutine def async_send_command(self, command): """Send a infrared command.""" - if command.startswith('01'): + if command.startswith("01"): yield from self._try_command( "Sending new air conditioner configuration failed.", - self._device.send_command, command) - elif command.startswith('FE'): + self._device.send_command, + command, + ) + elif command.startswith("FE"): if self._air_condition_model is not None: # Learned infrared commands has the prefix 'FE' yield from self._try_command( "Sending custom infrared command failed.", - self._device.send_ir_code, self._air_condition_model, command) + self._device.send_ir_code, + self._air_condition_model, + command, + ) else: - _LOGGER.error('Model number of the air condition unknown. ' - 'IR command cannot be sent.') + _LOGGER.error( + "Model number of the air condition unknown. " + "IR command cannot be sent." + ) else: - _LOGGER.error('Invalid IR command.') + _LOGGER.error("Invalid IR command.") From 4b223a46d735a183e67fe67fc45926551a718346 Mon Sep 17 00:00:00 2001 From: befantasy <31535803+befantasy@users.noreply.github.com> Date: Sat, 30 Nov 2019 14:01:25 +0800 Subject: [PATCH 37/39] Bump python-miio version (#90) --- .../xiaomi_miio_airconditioningcompanion/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json index ecb3e83..24f4d05 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json +++ b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json @@ -4,10 +4,10 @@ "documentation": "https://github.com/syssi/xiaomi_airconditioningcompanion", "requirements": [ "construct==2.9.45", - "python-miio==0.4.6" + "python-miio==0.4.7" ], "dependencies": ["sensor"], "codeowners": [ "@syssi" ] -} \ No newline at end of file +} From 4cce5fb3b7bcd406608e2a28173e1b5df50703c5 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 12 Dec 2019 18:14:08 +0100 Subject: [PATCH 38/39] Bump python-miio version --- .../xiaomi_miio_airconditioningcompanion/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json index 24f4d05..191a961 100644 --- a/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json +++ b/custom_components/xiaomi_miio_airconditioningcompanion/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://github.com/syssi/xiaomi_airconditioningcompanion", "requirements": [ "construct==2.9.45", - "python-miio==0.4.7" + "python-miio>=0.4.8" ], "dependencies": ["sensor"], "codeowners": [ From da8fda2542d37aa9531929f5448fa46e68ca63ff Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 1 Mar 2020 12:46:54 +0100 Subject: [PATCH 39/39] Add HACS support --- README.md | 6 ++++++ hacs.json | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 hacs.json diff --git a/README.md b/README.md index af8d39d..83beca1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all - fan_speed - swing_mode + +## Install + +You can install this custom component by adding this repository ([https://github.com/syssi/xiaomi_airconditioningcompanion](https://github.com/syssi/xiaomi_airconditioningcompanion/)) to [HACS](https://hacs.xyz/) in the settings menu of HACS first. You will find the custom component in the integration menu afterwards, look for 'Xiaomi Mi and Aqara Air Conditioning Companion Integration'. Alternatively, you can install it manually by copying the custom_component folder to your Home Assistant configuration folder. + + ## Setup ```yaml diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..12458fd --- /dev/null +++ b/hacs.json @@ -0,0 +1,6 @@ +{ + "name": "Xiaomi Mi and Aqara Air Conditioning Companion Integration", + "content_in_root": false, + "render_readme": true, + "iot_class": "local_polling" +}