Explorar el Código

Run code through isort and black, and add validation actions

Nik Rolls hace 5 años
padre
commit
986eda3902

+ 18 - 0
.github/workflows/hacs-validate.yml

@@ -0,0 +1,18 @@
+name: Validate with HACS
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  validate:
+    runs-on: "ubuntu-latest"
+    steps:
+      - uses: "actions/checkout@v2"
+      - name: HACS validation
+        uses: "hacs/integration/action@master"
+        with:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          CATEGORY: "integration"

+ 14 - 0
.github/workflows/hassfest-validate.yml

@@ -0,0 +1,14 @@
+name: Validate with hassfest
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  validate:
+    runs-on: "ubuntu-latest"
+    steps:
+      - uses: "actions/checkout@v2"
+      - uses: home-assistant/actions/hassfest@master

+ 44 - 41
custom_components/goldair_climate/__init__.py

@@ -5,41 +5,39 @@ Based on sean6541/tuya-homeassistant for service call logic, and TarxBoy's
 investigation into Goldair's tuyapi statuses
 https://github.com/codetheweb/tuyapi/issues/31.
 """
-import voluptuous as vol
+import logging
 
 import homeassistant.helpers.config_validation as cv
-from homeassistant.const import (CONF_NAME, CONF_HOST)
+import voluptuous as vol
+from homeassistant.const import CONF_HOST, CONF_NAME
 from homeassistant.helpers.discovery import async_load_platform
 
-from .const import (
-    CONF_DEVICE_ID, CONF_LOCAL_KEY, CONF_TYPE, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN,
-    CONF_CLIMATE, CONF_DISPLAY_LIGHT, CONF_CHILD_LOCK
-)
+from .const import (CONF_CHILD_LOCK, CONF_CLIMATE, CONF_DEVICE_ID,
+                    CONF_DISPLAY_LIGHT, CONF_LOCAL_KEY, CONF_TYPE,
+                    CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_HEATER)
 from .device import GoldairTuyaDevice
 
-import logging
-
 _LOGGER = logging.getLogger(__name__)
 
-VERSION = '0.0.8'
+VERSION = "0.0.8"
 
-DOMAIN = 'goldair_climate'
-DATA_GOLDAIR_CLIMATE = 'data_goldair_climate'
+DOMAIN = "goldair_climate"
+DATA_GOLDAIR_CLIMATE = "data_goldair_climate"
 
 INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
-    {'key': CONF_NAME, 'type': str, 'required': True},
-    {'key': CONF_HOST, 'type': str, 'required': True},
-    {'key': CONF_DEVICE_ID, 'type': str, 'required': True, 'fixed': True},
-    {'key': CONF_LOCAL_KEY, 'type': str, 'required': True},
+    {"key": CONF_NAME, "type": str, "required": True},
+    {"key": CONF_HOST, "type": str, "required": True},
+    {"key": CONF_DEVICE_ID, "type": str, "required": True, "fixed": True},
+    {"key": CONF_LOCAL_KEY, "type": str, "required": True},
     {
-        'key': CONF_TYPE,
-        'type': vol.In([CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN]),
-        'required': True,
-        'fixed': True
+        "key": CONF_TYPE,
+        "type": vol.In([CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN]),
+        "required": True,
+        "fixed": True,
     },
-    {'key': CONF_CLIMATE, 'type': bool, 'required': False, 'default': True},
-    {'key': CONF_DISPLAY_LIGHT, 'type': bool, 'required': False, 'default': False},
-    {'key': CONF_CHILD_LOCK, 'type': bool, 'required': False, 'default': False}
+    {"key": CONF_CLIMATE, "type": bool, "required": False, "default": True},
+    {"key": CONF_DISPLAY_LIGHT, "type": bool, "required": False, "default": False},
+    {"key": CONF_CHILD_LOCK, "type": bool, "required": False, "default": False},
 ]
 
 
@@ -47,24 +45,29 @@ def individual_config_schema(defaults={}, exclude_fixed=False):
     output = {}
 
     for prop in INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE:
-        if exclude_fixed and prop.get('fixed'):
+        if exclude_fixed and prop.get("fixed"):
             continue
 
         options = {}
 
-        default = defaults.get(prop['key'], prop.get('default'))
+        default = defaults.get(prop["key"], prop.get("default"))
         if default is not None:
-            options['default'] = default
+            options["default"] = default
 
-        key = vol.Required(prop['key'], **options) if prop['required'] else vol.Optional(prop['key'], **options)
-        output[key] = prop['type']
+        key = (
+            vol.Required(prop["key"], **options)
+            if prop["required"]
+            else vol.Optional(prop["key"], **options)
+        )
+        output[key] = prop["type"]
 
     return output
 
 
-CONFIG_SCHEMA = vol.Schema({
-    DOMAIN: vol.All(cv.ensure_list, [vol.Schema(individual_config_schema())])
-}, extra=vol.ALLOW_EXTRA)
+CONFIG_SCHEMA = vol.Schema(
+    {DOMAIN: vol.All(cv.ensure_list, [vol.Schema(individual_config_schema())])},
+    extra=vol.ALLOW_EXTRA,
+)
 
 
 async def async_setup(hass, config):
@@ -75,20 +78,20 @@ async def async_setup(hass, config):
 
         discovery_info = {
             CONF_DEVICE_ID: device_config.get(CONF_DEVICE_ID),
-            CONF_TYPE: device_config.get(CONF_TYPE)
+            CONF_TYPE: device_config.get(CONF_TYPE),
         }
 
         if device_config.get(CONF_CLIMATE) == True:
             hass.async_create_task(
-                async_load_platform(hass, 'climate', DOMAIN, discovery_info, config)
+                async_load_platform(hass, "climate", DOMAIN, discovery_info, config)
             )
         if device_config.get(CONF_DISPLAY_LIGHT) == True:
             hass.async_create_task(
-                async_load_platform(hass, 'light', DOMAIN, discovery_info, config)
+                async_load_platform(hass, "light", DOMAIN, discovery_info, config)
             )
         if device_config.get(CONF_CHILD_LOCK) == True:
             hass.async_create_task(
-                async_load_platform(hass, 'lock', DOMAIN, discovery_info, config)
+                async_load_platform(hass, "lock", DOMAIN, discovery_info, config)
             )
 
     return True
@@ -100,15 +103,15 @@ async def async_setup_entry(hass, entry):
 
     if config[CONF_CLIMATE] == True:
         hass.async_create_task(
-            hass.config_entries.async_forward_entry_setup(entry, 'climate')
+            hass.config_entries.async_forward_entry_setup(entry, "climate")
         )
     if config[CONF_DISPLAY_LIGHT] == True:
         hass.async_create_task(
-            hass.config_entries.async_forward_entry_setup(entry, 'light')
+            hass.config_entries.async_forward_entry_setup(entry, "light")
         )
     if config[CONF_CHILD_LOCK] == True:
         hass.async_create_task(
-            hass.config_entries.async_forward_entry_setup(entry, 'lock')
+            hass.config_entries.async_forward_entry_setup(entry, "lock")
         )
 
     entry.add_update_listener(async_update_entry)
@@ -121,11 +124,11 @@ async def async_unload_entry(hass, entry):
     delete_device(hass, config)
 
     if config[CONF_CLIMATE] == True:
-        await hass.config_entries.async_forward_entry_unload(entry, 'climate')
+        await hass.config_entries.async_forward_entry_unload(entry, "climate")
     if config[CONF_DISPLAY_LIGHT] == True:
-        await hass.config_entries.async_forward_entry_unload(entry, 'light')
+        await hass.config_entries.async_forward_entry_unload(entry, "light")
     if config[CONF_CHILD_LOCK] == True:
-        await hass.config_entries.async_forward_entry_unload(entry, 'lock')
+        await hass.config_entries.async_forward_entry_unload(entry, "lock")
 
     return True
 
@@ -141,7 +144,7 @@ def setup_device(hass, config):
         config.get(CONF_DEVICE_ID),
         config.get(CONF_HOST),
         config.get(CONF_LOCAL_KEY),
-        hass
+        hass,
     )
     hass.data[DOMAIN][config.get(CONF_DEVICE_ID)] = device
 

+ 4 - 3
custom_components/goldair_climate/climate.py

@@ -2,10 +2,11 @@
 Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
-from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN)
-from .heater.climate import GoldairHeater
+from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER)
 from .dehumidifier.climate import GoldairDehumidifier
 from .fan.climate import GoldairFan
+from .heater.climate import GoldairHeater
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -23,6 +24,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
     discovery_info = {
         CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE]
+        CONF_TYPE: config[CONF_TYPE],
     }
     await async_setup_platform(hass, {}, async_add_entities, discovery_info)

+ 12 - 15
custom_components/goldair_climate/config_flow.py

@@ -1,11 +1,11 @@
-import voluptuous as vol
 import homeassistant.helpers.config_validation as cv
-
+import voluptuous as vol
 from homeassistant import config_entries
+from homeassistant.const import CONF_HOST, CONF_NAME
 from homeassistant.core import callback
-from homeassistant.const import (CONF_NAME, CONF_HOST)
-from . import (DOMAIN, individual_config_schema)
-from .const import (CONF_DEVICE_ID, CONF_TYPE)
+
+from . import DOMAIN, individual_config_schema
+from .const import CONF_DEVICE_ID, CONF_TYPE
 
 
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -16,13 +16,10 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         if user_input is not None:
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
             self._abort_if_unique_id_configured()
-            return self.async_create_entry(
-                title=user_input[CONF_NAME], data=user_input
-            )
+            return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
 
         return self.async_show_form(
-            step_id='user',
-            data_schema=vol.Schema(individual_config_schema())
+            step_id="user", data_schema=vol.Schema(individual_config_schema())
         )
 
     @staticmethod
@@ -39,12 +36,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
     async def async_step_init(self, user_input=None):
         """Manage the options."""
         if user_input is not None:
-            return self.async_create_entry(
-                title=user_input[CONF_NAME], data=user_input
-            )
+            return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
 
         config = {**self.config_entry.data, **self.config_entry.options}
         return self.async_show_form(
-            step_id='user',
-            data_schema=vol.Schema(individual_config_schema(defaults=config, exclude_fixed=True))
+            step_id="user",
+            data_schema=vol.Schema(
+                individual_config_schema(defaults=config, exclude_fixed=True)
+            ),
         )

+ 9 - 9
custom_components/goldair_climate/const.py

@@ -1,11 +1,11 @@
-CONF_DEVICE_ID = 'device_id'
-CONF_LOCAL_KEY = 'local_key'
-CONF_TYPE = 'type'
-CONF_TYPE_HEATER = 'heater'
-CONF_TYPE_DEHUMIDIFIER = 'dehumidifier'
-CONF_TYPE_FAN = 'fan'
-CONF_CLIMATE = 'climate'
-CONF_DISPLAY_LIGHT = 'display_light'
-CONF_CHILD_LOCK = 'child_lock'
+CONF_DEVICE_ID = "device_id"
+CONF_LOCAL_KEY = "local_key"
+CONF_TYPE = "type"
+CONF_TYPE_HEATER = "heater"
+CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
+CONF_TYPE_FAN = "fan"
+CONF_CLIMATE = "climate"
+CONF_DISPLAY_LIGHT = "display_light"
+CONF_CHILD_LOCK = "child_lock"
 
 API_PROTOCOL_VERSIONS = [3.3, 3.1]

+ 74 - 29
custom_components/goldair_climate/dehumidifier/climate.py

@@ -1,18 +1,36 @@
 """
 Goldair WiFi Dehumidifier device.
 """
-from homeassistant.const import (ATTR_TEMPERATURE, STATE_UNAVAILABLE)
 from homeassistant.components.climate import ClimateDevice
 from homeassistant.components.climate.const import (
-    ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_PRESET_MODE, FAN_LOW, FAN_HIGH, SUPPORT_TARGET_HUMIDITY,
-    SUPPORT_PRESET_MODE, SUPPORT_FAN_MODE
+    ATTR_FAN_MODE,
+    ATTR_HUMIDITY,
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    FAN_HIGH,
+    FAN_LOW,
+    SUPPORT_FAN_MODE,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_HUMIDITY,
 )
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from ..device import GoldairTuyaDevice
 from .const import (
-    ATTR_TARGET_HUMIDITY, ATTR_AIR_CLEAN_ON, ATTR_ERROR, PRESET_NORMAL, PRESET_LOW, PRESET_HIGH, PRESET_DRY_CLOTHES,
-    PRESET_AIR_CLEAN, PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE, PRESET_MODE_TO_DPS_MODE, FAN_MODE_TO_DPS_MODE,
-    ERROR_CODE_TO_DPS_CODE
+    ATTR_AIR_CLEAN_ON,
+    ATTR_ERROR,
+    ATTR_TARGET_HUMIDITY,
+    ERROR_CODE_TO_DPS_CODE,
+    FAN_MODE_TO_DPS_MODE,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_AIR_CLEAN,
+    PRESET_DRY_CLOTHES,
+    PRESET_HIGH,
+    PRESET_LOW,
+    PRESET_MODE_TO_DPS_MODE,
+    PRESET_NORMAL,
+    PROPERTY_TO_DPS_ID,
 )
-from ..device import GoldairTuyaDevice
 
 SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY | SUPPORT_PRESET_MODE | SUPPORT_FAN_MODE
 
@@ -30,10 +48,7 @@ class GoldairDehumidifier(ClimateDevice):
         self._support_flags = SUPPORT_FLAGS
 
         self._HUMIDITY_STEP = 5
-        self._HUMIDITY_LIMITS = {
-            'min': 30,
-            'max': 80
-        }
+        self._HUMIDITY_LIMITS = {"min": 30, "max": 80}
 
     @property
     def supported_features(self):
@@ -58,12 +73,12 @@ class GoldairDehumidifier(ClimateDevice):
     @property
     def min_humidity(self):
         """Return the minimum humidity setting."""
-        return self._HUMIDITY_LIMITS['min']
+        return self._HUMIDITY_LIMITS["min"]
 
     @property
     def max_humidity(self):
         """Return the maximum humidity setting."""
-        return self._HUMIDITY_LIMITS['max']
+        return self._HUMIDITY_LIMITS["max"]
 
     @property
     def target_humidity(self):
@@ -73,9 +88,15 @@ class GoldairDehumidifier(ClimateDevice):
     async def async_set_humidity(self, humidity):
         """Set the device's target humidity."""
         if self.preset_mode in [PRESET_AIR_CLEAN, PRESET_DRY_CLOTHES]:
-            raise ValueError('Humidity can only be changed while in Normal, Low or High preset modes.')
-        humidity = int(self._HUMIDITY_STEP * round(float(humidity) / self._HUMIDITY_STEP))
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY], humidity)
+            raise ValueError(
+                "Humidity can only be changed while in Normal, Low or High preset modes."
+            )
+        humidity = int(
+            self._HUMIDITY_STEP * round(float(humidity) / self._HUMIDITY_STEP)
+        )
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_TARGET_HUMIDITY], humidity
+        )
 
     @property
     def temperature_unit(self):
@@ -115,7 +136,9 @@ class GoldairDehumidifier(ClimateDevice):
     async def async_set_hvac_mode(self, hvac_mode):
         """Set new HVAC mode."""
         dps_mode = HVAC_MODE_TO_DPS_MODE[hvac_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode
+        )
 
     @property
     def preset_mode(self):
@@ -126,7 +149,9 @@ class GoldairDehumidifier(ClimateDevice):
         if air_clean_on:
             return PRESET_AIR_CLEAN
         elif dps_mode is not None:
-            return GoldairTuyaDevice.get_key_for_value(PRESET_MODE_TO_DPS_MODE, dps_mode)
+            return GoldairTuyaDevice.get_key_for_value(
+                PRESET_MODE_TO_DPS_MODE, dps_mode
+            )
         else:
             return None
 
@@ -138,16 +163,28 @@ class GoldairDehumidifier(ClimateDevice):
     async def async_set_preset_mode(self, preset_mode):
         """Set new preset mode."""
         if preset_mode == PRESET_AIR_CLEAN:
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON], True)
-            self._device.anticipate_property_value(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH)
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON], True
+            )
+            self._device.anticipate_property_value(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH
+            )
         else:
             dps_mode = PRESET_MODE_TO_DPS_MODE[preset_mode]
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON], False)
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode)
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_AIR_CLEAN_ON], False
+            )
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode
+            )
             if preset_mode == PRESET_LOW:
-                self._device.anticipate_property_value(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_LOW)
+                self._device.anticipate_property_value(
+                    PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_LOW
+                )
             elif preset_mode in [PRESET_HIGH, PRESET_DRY_CLOTHES]:
-                self._device.anticipate_property_value(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH)
+                self._device.anticipate_property_value(
+                    PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], FAN_HIGH
+                )
 
     @property
     def fan_mode(self):
@@ -161,7 +198,9 @@ class GoldairDehumidifier(ClimateDevice):
         else:
             dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE])
             if dps_mode is not None:
-                return GoldairTuyaDevice.get_key_for_value(FAN_MODE_TO_DPS_MODE, dps_mode)
+                return GoldairTuyaDevice.get_key_for_value(
+                    FAN_MODE_TO_DPS_MODE, dps_mode
+                )
             else:
                 return None
 
@@ -182,20 +221,26 @@ class GoldairDehumidifier(ClimateDevice):
     async def async_set_fan_mode(self, fan_mode):
         """Set new fan mode."""
         if self.preset_mode != PRESET_NORMAL:
-            raise ValueError('Fan mode can only be changed while in Normal preset mode.')
+            raise ValueError(
+                "Fan mode can only be changed while in Normal preset mode."
+            )
 
         if fan_mode not in FAN_MODE_TO_DPS_MODE.keys():
-            raise ValueError(f'Invalid fan mode: {fan_mode}')
+            raise ValueError(f"Invalid fan mode: {fan_mode}")
 
         dps_mode = FAN_MODE_TO_DPS_MODE[fan_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], dps_mode
+        )
 
     @property
     def device_state_attributes(self):
         """Get additional attributes that HA doesn't naturally support."""
         error = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_ERROR])
         if error:
-            error = GoldairTuyaDevice.get_key_for_value(ERROR_CODE_TO_DPS_CODE, error, error)
+            error = GoldairTuyaDevice.get_key_for_value(
+                ERROR_CODE_TO_DPS_CODE, error, error
+            )
 
         return {ATTR_ERROR: error or None}
 

+ 38 - 40
custom_components/goldair_climate/dehumidifier/const.py

@@ -1,51 +1,49 @@
-from homeassistant.const import ATTR_TEMPERATURE
 from homeassistant.components.climate.const import (
-    ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_PRESET_MODE, FAN_LOW, FAN_HIGH, HVAC_MODE_OFF, HVAC_MODE_DRY
+    ATTR_FAN_MODE,
+    ATTR_HUMIDITY,
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    FAN_HIGH,
+    FAN_LOW,
+    HVAC_MODE_DRY,
+    HVAC_MODE_OFF,
 )
+from homeassistant.const import ATTR_TEMPERATURE
 
-ATTR_TARGET_HUMIDITY = 'target_humidity'
-ATTR_AIR_CLEAN_ON = 'air_clean_on'
-ATTR_CHILD_LOCK = 'child_lock'
-ATTR_ERROR = 'error'
-ATTR_DISPLAY_ON = 'display_on'
+ATTR_TARGET_HUMIDITY = "target_humidity"
+ATTR_AIR_CLEAN_ON = "air_clean_on"
+ATTR_CHILD_LOCK = "child_lock"
+ATTR_ERROR = "error"
+ATTR_DISPLAY_ON = "display_on"
 
-PRESET_NORMAL = 'Normal'
-PRESET_LOW = 'Low'
-PRESET_HIGH = 'High'
-PRESET_DRY_CLOTHES = 'Dry clothes'
-PRESET_AIR_CLEAN = 'Air clean'
+PRESET_NORMAL = "Normal"
+PRESET_LOW = "Low"
+PRESET_HIGH = "High"
+PRESET_DRY_CLOTHES = "Dry clothes"
+PRESET_AIR_CLEAN = "Air clean"
 
-ERROR_NONE = 'No error'
-ERROR_TANK = 'Tank full or missing'
+ERROR_NONE = "No error"
+ERROR_TANK = "Tank full or missing"
 
 PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: '1',
-    ATTR_PRESET_MODE: '2',
-    ATTR_TARGET_HUMIDITY: '4',
-    ATTR_AIR_CLEAN_ON: '5',
-    ATTR_FAN_MODE: '6',
-    ATTR_CHILD_LOCK: '7',
-    ATTR_ERROR: '11',
-    ATTR_DISPLAY_ON: '102',
-    ATTR_TEMPERATURE: '103',
-    ATTR_HUMIDITY: '104'
+    ATTR_HVAC_MODE: "1",
+    ATTR_PRESET_MODE: "2",
+    ATTR_TARGET_HUMIDITY: "4",
+    ATTR_AIR_CLEAN_ON: "5",
+    ATTR_FAN_MODE: "6",
+    ATTR_CHILD_LOCK: "7",
+    ATTR_ERROR: "11",
+    ATTR_DISPLAY_ON: "102",
+    ATTR_TEMPERATURE: "103",
+    ATTR_HUMIDITY: "104",
 }
 
-HVAC_MODE_TO_DPS_MODE = {
-    HVAC_MODE_OFF: False,
-    HVAC_MODE_DRY: True
-}
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_DRY: True}
 PRESET_MODE_TO_DPS_MODE = {
-    PRESET_NORMAL: '0',
-    PRESET_LOW: '1',
-    PRESET_HIGH: '2',
-    PRESET_DRY_CLOTHES: '3'
-}
-FAN_MODE_TO_DPS_MODE = {
-    FAN_LOW: '1',
-    FAN_HIGH: '3'
-}
-ERROR_CODE_TO_DPS_CODE = {
-    ERROR_NONE: 0,
-    ERROR_TANK: 8
+    PRESET_NORMAL: "0",
+    PRESET_LOW: "1",
+    PRESET_HIGH: "2",
+    PRESET_DRY_CLOTHES: "3",
 }
+FAN_MODE_TO_DPS_MODE = {FAN_LOW: "1", FAN_HIGH: "3"}
+ERROR_CODE_TO_DPS_CODE = {ERROR_NONE: 0, ERROR_TANK: 8}

+ 10 - 8
custom_components/goldair_climate/dehumidifier/light.py

@@ -1,15 +1,12 @@
 """
 Platform to control the LED display light on Goldair WiFi-connected dehumidifiers.
 """
+from homeassistant.components.climate import ATTR_HVAC_MODE, HVAC_MODE_OFF
 from homeassistant.components.light import Light
 from homeassistant.const import STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
-from .const import (
-    ATTR_DISPLAY_ON, PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE
-)
-from homeassistant.components.climate import (
-    ATTR_HVAC_MODE, HVAC_MODE_OFF
-)
+from .const import ATTR_DISPLAY_ON, HVAC_MODE_TO_DPS_MODE, PROPERTY_TO_DPS_ID
 
 
 class GoldairDehumidifierLedDisplayLight(Light):
@@ -37,7 +34,10 @@ class GoldairDehumidifierLedDisplayLight(Light):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
         dps_display_on = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON])
 
-        if dps_hvac_mode is None or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]:
+        if (
+            dps_hvac_mode is None
+            or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]
+        ):
             return STATE_UNAVAILABLE
         else:
             return dps_display_on
@@ -46,7 +46,9 @@ class GoldairDehumidifierLedDisplayLight(Light):
         await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], True)
 
     async def async_turn_off(self):
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False
+        )
 
     async def async_toggle(self):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])

+ 6 - 3
custom_components/goldair_climate/dehumidifier/lock.py

@@ -1,10 +1,11 @@
 """
 Platform to control the child lock on Goldair WiFi-connected dehumidifiers.
 """
-from homeassistant.components.lock import (STATE_LOCKED, STATE_UNLOCKED, LockDevice)
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockDevice
 from homeassistant.const import STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
-from .const import (ATTR_CHILD_LOCK, PROPERTY_TO_DPS_ID)
+from .const import ATTR_CHILD_LOCK, PROPERTY_TO_DPS_ID
 
 
 class GoldairDehumidifierChildLock(LockDevice):
@@ -45,7 +46,9 @@ class GoldairDehumidifierChildLock(LockDevice):
 
     async def async_unlock(self, **kwargs):
         """Turn off the child lock."""
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], False)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], False
+        )
 
     async def async_update(self):
         await self._device.async_refresh()

+ 36 - 30
custom_components/goldair_climate/device.py

@@ -2,12 +2,13 @@
 API for Goldair Tuya devices.
 """
 
-from time import time
-from threading import Timer, Lock
-import logging
 import json
+import logging
+from threading import Lock, Timer
+from time import time
 
 from homeassistant.const import TEMP_CELSIUS
+
 from .const import API_PROTOCOL_VERSIONS
 
 _LOGGER = logging.getLogger(__name__)
@@ -24,9 +25,10 @@ class GoldairTuyaDevice(object):
             local_key (str): The encryption key.
         """
         import pytuya
+
         self._name = name
         self._api_protocol_version_index = None
-        self._api = pytuya.Device(dev_id, address, local_key, 'device')
+        self._api = pytuya.Device(dev_id, address, local_key, "device")
         self._rotate_api_protocol_version()
 
         self._fixed_properties = {}
@@ -55,17 +57,19 @@ class GoldairTuyaDevice(object):
 
     def set_fixed_properties(self, fixed_properties):
         self._fixed_properties = fixed_properties
-        set_fixed_properties = Timer(10, lambda: self._set_properties(self._fixed_properties))
+        set_fixed_properties = Timer(
+            10, lambda: self._set_properties(self._fixed_properties)
+        )
         set_fixed_properties.start()
 
     def refresh(self):
         now = time()
         cached_state = self._get_cached_state()
-        if now - cached_state['updated_at'] >= self._CACHE_TIMEOUT:
-            self._cached_state['updated_at'] = time()
+        if now - cached_state["updated_at"] >= self._CACHE_TIMEOUT:
+            self._cached_state["updated_at"] = time()
             self._retry_on_failed_connection(
                 lambda: self._refresh_cached_state(),
-                f'Failed to refresh device state for {self.name}.'
+                f"Failed to refresh device state for {self.name}.",
             )
 
     async def async_refresh(self):
@@ -94,17 +98,17 @@ class GoldairTuyaDevice(object):
         self._cached_state[dps_id] = value
 
     def _reset_cached_state(self):
-        self._cached_state = {
-            'updated_at': 0
-        }
+        self._cached_state = {"updated_at": 0}
         self._pending_updates = {}
 
     def _refresh_cached_state(self):
         new_state = self._api.status()
-        self._cached_state = new_state['dps']
-        self._cached_state['updated_at'] = time()
-        _LOGGER.info(f'refreshed device state: {json.dumps(new_state)}')
-        _LOGGER.debug(f'new cache state (including pending properties): {json.dumps(self._get_cached_state())}')
+        self._cached_state = new_state["dps"]
+        self._cached_state["updated_at"] = time()
+        _LOGGER.info(f"refreshed device state: {json.dumps(new_state)}")
+        _LOGGER.debug(
+            f"new cache state (including pending properties): {json.dumps(self._get_cached_state())}"
+        )
 
     def _set_properties(self, properties):
         if len(properties) == 0:
@@ -119,12 +123,9 @@ class GoldairTuyaDevice(object):
 
         pending_updates = self._get_pending_updates()
         for key, value in properties.items():
-            pending_updates[key] = {
-                'value': value,
-                'updated_at': now
-            }
+            pending_updates[key] = {"value": value, "updated_at": now}
 
-        _LOGGER.debug(f'new pending updates: {json.dumps(self._pending_updates)}')
+        _LOGGER.debug(f"new pending updates: {json.dumps(self._pending_updates)}")
 
     def _debounce_sending_updates(self):
         try:
@@ -136,21 +137,23 @@ class GoldairTuyaDevice(object):
 
     def _send_pending_updates(self):
         pending_properties = self._get_pending_properties()
-        payload = self._api.generate_payload('set', pending_properties)
+        payload = self._api.generate_payload("set", pending_properties)
 
-        _LOGGER.info(f'sending dps update: {json.dumps(pending_properties)}')
+        _LOGGER.info(f"sending dps update: {json.dumps(pending_properties)}")
 
-        self._retry_on_failed_connection(lambda: self._send_payload(payload), 'Failed to update device state.')
+        self._retry_on_failed_connection(
+            lambda: self._send_payload(payload), "Failed to update device state."
+        )
 
     def _send_payload(self, payload):
         try:
             self._lock.acquire()
             self._api._send_receive(payload)
-            self._cached_state['updated_at'] = 0
+            self._cached_state["updated_at"] = 0
             now = time()
             pending_updates = self._get_pending_updates()
             for key, value in pending_updates.items():
-                pending_updates[key]['updated_at'] = now
+                pending_updates[key]["updated_at"] = now
         finally:
             self._lock.release()
 
@@ -167,16 +170,19 @@ class GoldairTuyaDevice(object):
 
     def _get_cached_state(self):
         cached_state = self._cached_state.copy()
-        _LOGGER.debug(f'pending updates: {json.dumps(self._get_pending_updates())}')
+        _LOGGER.debug(f"pending updates: {json.dumps(self._get_pending_updates())}")
         return {**cached_state, **self._get_pending_properties()}
 
     def _get_pending_properties(self):
-        return {key: info['value'] for key, info in self._get_pending_updates().items()}
+        return {key: info["value"] for key, info in self._get_pending_updates().items()}
 
     def _get_pending_updates(self):
         now = time()
-        self._pending_updates = {key: value for key, value in self._pending_updates.items()
-                                 if now - value['updated_at'] < self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT}
+        self._pending_updates = {
+            key: value
+            for key, value in self._pending_updates.items()
+            if now - value["updated_at"] < self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT
+        }
         return self._pending_updates
 
     def _rotate_api_protocol_version(self):
@@ -189,7 +195,7 @@ class GoldairTuyaDevice(object):
             self._api_protocol_version_index = 0
 
         new_version = API_PROTOCOL_VERSIONS[self._api_protocol_version_index]
-        _LOGGER.info(f'Setting protocol version for {self.name} to {new_version}.')
+        _LOGGER.info(f"Setting protocol version for {self.name} to {new_version}.")
         self._api.set_version(new_version)
 
     @staticmethod

+ 37 - 13
custom_components/goldair_climate/fan/climate.py

@@ -1,17 +1,25 @@
 """
 Goldair WiFi Fan device.
 """
-from homeassistant.const import (
-    ATTR_TEMPERATURE, TEMP_CELSIUS, STATE_UNAVAILABLE
-)
 from homeassistant.components.climate import ClimateDevice
 from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE,
-    SUPPORT_SWING_MODE
+    ATTR_FAN_MODE,
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    ATTR_SWING_MODE,
+    SUPPORT_FAN_MODE,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_SWING_MODE,
 )
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE, TEMP_CELSIUS
+
 from ..device import GoldairTuyaDevice
 from .const import (
-    PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE, PRESET_MODE_TO_DPS_MODE, SWING_MODE_TO_DPS_MODE, FAN_MODES
+    FAN_MODES,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+    SWING_MODE_TO_DPS_MODE,
 )
 
 SUPPORT_FLAGS = SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE | SUPPORT_SWING_MODE
@@ -67,14 +75,18 @@ class GoldairFan(ClimateDevice):
     async def async_set_hvac_mode(self, hvac_mode):
         """Set new HVAC mode."""
         dps_mode = HVAC_MODE_TO_DPS_MODE[hvac_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode
+        )
 
     @property
     def preset_mode(self):
         """Return current preset mode, ie Comfort, Eco, Anti-freeze."""
         dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE])
         if dps_mode is not None:
-            return GoldairTuyaDevice.get_key_for_value(PRESET_MODE_TO_DPS_MODE, dps_mode)
+            return GoldairTuyaDevice.get_key_for_value(
+                PRESET_MODE_TO_DPS_MODE, dps_mode
+            )
         else:
             return None
 
@@ -86,7 +98,9 @@ class GoldairFan(ClimateDevice):
     async def async_set_preset_mode(self, preset_mode):
         """Set new preset mode."""
         dps_mode = PRESET_MODE_TO_DPS_MODE[preset_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode
+        )
 
     @property
     def swing_mode(self):
@@ -105,14 +119,22 @@ class GoldairFan(ClimateDevice):
     async def async_set_swing_mode(self, swing_mode):
         """Set new swing mode."""
         dps_mode = SWING_MODE_TO_DPS_MODE[swing_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_SWING_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_SWING_MODE], dps_mode
+        )
 
     @property
     def fan_mode(self):
         """Return current fan mode: 1-12"""
         dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE])
-        if dps_mode is not None and self.preset_mode is not None and dps_mode in FAN_MODES[self.preset_mode].values():
-            return GoldairTuyaDevice.get_key_for_value(FAN_MODES[self.preset_mode], dps_mode)
+        if (
+            dps_mode is not None
+            and self.preset_mode is not None
+            and dps_mode in FAN_MODES[self.preset_mode].values()
+        ):
+            return GoldairTuyaDevice.get_key_for_value(
+                FAN_MODES[self.preset_mode], dps_mode
+            )
         else:
             return None
 
@@ -128,7 +150,9 @@ class GoldairFan(ClimateDevice):
         """Set new fan mode."""
         if self.preset_mode is not None:
             dps_mode = FAN_MODES[self.preset_mode][int(fan_mode)]
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], dps_mode)
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_FAN_MODE], dps_mode
+            )
 
     async def async_update(self):
         await self._device.async_refresh()

+ 39 - 27
custom_components/goldair_climate/fan/const.py

@@ -1,39 +1,51 @@
 from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE,
-    HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY,
-    PRESET_ECO, PRESET_SLEEP,
-    SWING_OFF, SWING_HORIZONTAL
+    ATTR_FAN_MODE,
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    ATTR_SWING_MODE,
+    HVAC_MODE_FAN_ONLY,
+    HVAC_MODE_OFF,
+    PRESET_ECO,
+    PRESET_SLEEP,
+    SWING_HORIZONTAL,
+    SWING_OFF,
 )
 
-ATTR_TARGET_TEMPERATURE = 'target_temperature'
-ATTR_DISPLAY_ON = 'display_on'
+ATTR_TARGET_TEMPERATURE = "target_temperature"
+ATTR_DISPLAY_ON = "display_on"
 
-PRESET_NORMAL = 'normal'
+PRESET_NORMAL = "normal"
 
 PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: '1',
-    ATTR_FAN_MODE: '2',
-    ATTR_PRESET_MODE: '3',
-    ATTR_SWING_MODE: '8',
-    ATTR_DISPLAY_ON: '101'
+    ATTR_HVAC_MODE: "1",
+    ATTR_FAN_MODE: "2",
+    ATTR_PRESET_MODE: "3",
+    ATTR_SWING_MODE: "8",
+    ATTR_DISPLAY_ON: "101",
 }
 
-HVAC_MODE_TO_DPS_MODE = {
-    HVAC_MODE_OFF: False,
-    HVAC_MODE_FAN_ONLY: True
-}
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_FAN_ONLY: True}
 PRESET_MODE_TO_DPS_MODE = {
-    PRESET_NORMAL: 'normal',
-    PRESET_ECO: 'nature',
-    PRESET_SLEEP: 'sleep'
-}
-SWING_MODE_TO_DPS_MODE = {
-    SWING_OFF: False,
-    SWING_HORIZONTAL: True
+    PRESET_NORMAL: "normal",
+    PRESET_ECO: "nature",
+    PRESET_SLEEP: "sleep",
 }
+SWING_MODE_TO_DPS_MODE = {SWING_OFF: False, SWING_HORIZONTAL: True}
 FAN_MODES = {
-    PRESET_NORMAL: {1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11',
-                    12: '12'},
-    PRESET_ECO: {1: '4', 2: '8', 3: '12'},
-    PRESET_SLEEP: {1: '4', 2: '8', 3: '12'}
+    PRESET_NORMAL: {
+        1: "1",
+        2: "2",
+        3: "3",
+        4: "4",
+        5: "5",
+        6: "6",
+        7: "7",
+        8: "8",
+        9: "9",
+        10: "10",
+        11: "11",
+        12: "12",
+    },
+    PRESET_ECO: {1: "4", 2: "8", 3: "12"},
+    PRESET_SLEEP: {1: "4", 2: "8", 3: "12"},
 }

+ 13 - 9
custom_components/goldair_climate/fan/light.py

@@ -1,15 +1,12 @@
 """
 Platform to control the LED display light on Goldair WiFi-connected fans and panels.
 """
+from homeassistant.components.climate import ATTR_HVAC_MODE, HVAC_MODE_OFF
 from homeassistant.components.light import Light
 from homeassistant.const import STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
-from .const import (
-    ATTR_DISPLAY_ON, PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE
-)
-from homeassistant.components.climate import (
-    ATTR_HVAC_MODE, HVAC_MODE_OFF
-)
+from .const import ATTR_DISPLAY_ON, HVAC_MODE_TO_DPS_MODE, PROPERTY_TO_DPS_ID
 
 
 class GoldairFanLedDisplayLight(Light):
@@ -37,7 +34,10 @@ class GoldairFanLedDisplayLight(Light):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
         dps_display_on = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON])
 
-        if dps_hvac_mode is None or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]:
+        if (
+            dps_hvac_mode is None
+            or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]
+        ):
             return STATE_UNAVAILABLE
         else:
             return dps_display_on
@@ -46,14 +46,18 @@ class GoldairFanLedDisplayLight(Light):
         await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], True)
 
     async def async_turn_off(self):
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False
+        )
 
     async def async_toggle(self):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
         dps_display_on = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON])
 
         if dps_hvac_mode != HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]:
-            await (self.async_turn_on() if not dps_display_on else self.async_turn_off())
+            await (
+                self.async_turn_on() if not dps_display_on else self.async_turn_off()
+            )
 
     async def async_update(self):
         await self._device.async_refresh()

+ 56 - 29
custom_components/goldair_climate/heater/climate.py

@@ -1,17 +1,32 @@
 """
 Goldair WiFi Heater device.
 """
-from homeassistant.const import (ATTR_TEMPERATURE, STATE_UNAVAILABLE)
 from homeassistant.components.climate import ClimateDevice
 from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE, ATTR_PRESET_MODE,
-    SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_SWING_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
 )
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
 from .const import (
-    ATTR_TARGET_TEMPERATURE, ATTR_ERROR, ATTR_POWER_MODE_AUTO, ATTR_POWER_MODE_USER, ATTR_POWER_LEVEL, ATTR_POWER_MODE,
-    ATTR_ECO_TARGET_TEMPERATURE, STATE_COMFORT, STATE_ECO, STATE_ANTI_FREEZE, PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE,
-    PRESET_MODE_TO_DPS_MODE, POWER_LEVEL_TO_DPS_LEVEL
+    ATTR_ECO_TARGET_TEMPERATURE,
+    ATTR_ERROR,
+    ATTR_POWER_LEVEL,
+    ATTR_POWER_MODE,
+    ATTR_POWER_MODE_AUTO,
+    ATTR_POWER_MODE_USER,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    POWER_LEVEL_TO_DPS_LEVEL,
+    PRESET_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+    STATE_ANTI_FREEZE,
+    STATE_COMFORT,
+    STATE_ECO,
 )
 
 SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_SWING_MODE
@@ -31,14 +46,8 @@ class GoldairHeater(ClimateDevice):
 
         self._TEMPERATURE_STEP = 1
         self._TEMPERATURE_LIMITS = {
-            STATE_COMFORT: {
-                'min': 5,
-                'max': 35
-            },
-            STATE_ECO: {
-                'min': 5,
-                'max': 21
-            }
+            STATE_COMFORT: {"min": 5, "max": 35},
+            STATE_ECO: {"min": 5, "max": 21},
         }
 
     @property
@@ -65,9 +74,13 @@ class GoldairHeater(ClimateDevice):
     def target_temperature(self):
         """Return the temperature we try to reach."""
         if self.preset_mode == STATE_COMFORT:
-            return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE])
+            return self._device.get_property(
+                PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]
+            )
         elif self.preset_mode == STATE_ECO:
-            return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE])
+            return self._device.get_property(
+                PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE]
+            )
         else:
             return None
 
@@ -80,7 +93,7 @@ class GoldairHeater(ClimateDevice):
     def min_temp(self):
         """Return the minimum temperature."""
         if self.preset_mode and self.preset_mode != STATE_ANTI_FREEZE:
-            return self._TEMPERATURE_LIMITS[self.preset_mode]['min']
+            return self._TEMPERATURE_LIMITS[self.preset_mode]["min"]
         else:
             return None
 
@@ -88,7 +101,7 @@ class GoldairHeater(ClimateDevice):
     def max_temp(self):
         """Return the maximum temperature."""
         if self.preset_mode and self.preset_mode != STATE_ANTI_FREEZE:
-            return self._TEMPERATURE_LIMITS[self.preset_mode]['max']
+            return self._TEMPERATURE_LIMITS[self.preset_mode]["max"]
         else:
             return None
 
@@ -104,19 +117,23 @@ class GoldairHeater(ClimateDevice):
         preset_mode = self.preset_mode
 
         if preset_mode == STATE_ANTI_FREEZE:
-            raise ValueError('You cannot set the temperature in Anti-freeze mode.')
+            raise ValueError("You cannot set the temperature in Anti-freeze mode.")
 
         limits = self._TEMPERATURE_LIMITS[preset_mode]
-        if not limits['min'] <= target_temperature <= limits['max']:
+        if not limits["min"] <= target_temperature <= limits["max"]:
             raise ValueError(
-                f'Target temperature ({target_temperature}) must be between '
+                f"Target temperature ({target_temperature}) must be between "
                 f'{limits["min"]} and {limits["max"]}'
             )
 
         if preset_mode == STATE_COMFORT:
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE], target_temperature)
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE], target_temperature
+            )
         elif preset_mode == STATE_ECO:
-            await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE], target_temperature)
+            await self._device.async_set_property(
+                PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE], target_temperature
+            )
 
     @property
     def current_temperature(self):
@@ -141,14 +158,18 @@ class GoldairHeater(ClimateDevice):
     async def async_set_hvac_mode(self, hvac_mode):
         """Set new HVAC mode."""
         dps_mode = HVAC_MODE_TO_DPS_MODE[hvac_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode
+        )
 
     @property
     def preset_mode(self):
         """Return current preset mode, ie Comfort, Eco, Anti-freeze."""
         dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE])
         if dps_mode is not None:
-            return GoldairTuyaDevice.get_key_for_value(PRESET_MODE_TO_DPS_MODE, dps_mode)
+            return GoldairTuyaDevice.get_key_for_value(
+                PRESET_MODE_TO_DPS_MODE, dps_mode
+            )
         else:
             return None
 
@@ -160,7 +181,9 @@ class GoldairHeater(ClimateDevice):
     async def async_set_preset_mode(self, preset_mode):
         """Set new preset mode."""
         dps_mode = PRESET_MODE_TO_DPS_MODE[preset_mode]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode
+        )
 
     @property
     def swing_mode(self):
@@ -169,7 +192,9 @@ class GoldairHeater(ClimateDevice):
         if dps_mode == ATTR_POWER_MODE_USER:
             return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL])
         elif dps_mode == ATTR_POWER_MODE_AUTO:
-            return GoldairTuyaDevice.get_key_for_value(POWER_LEVEL_TO_DPS_LEVEL, dps_mode)
+            return GoldairTuyaDevice.get_key_for_value(
+                POWER_LEVEL_TO_DPS_LEVEL, dps_mode
+            )
         else:
             return None
 
@@ -182,9 +207,11 @@ class GoldairHeater(ClimateDevice):
         """Set new power level."""
         new_level = swing_mode
         if new_level not in POWER_LEVEL_TO_DPS_LEVEL.keys():
-            raise ValueError(f'Invalid power level: {new_level}')
+            raise ValueError(f"Invalid power level: {new_level}")
         dps_level = POWER_LEVEL_TO_DPS_LEVEL[new_level]
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL], dps_level)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL], dps_level
+        )
 
     @property
     def device_state_attributes(self):

+ 38 - 38
custom_components/goldair_climate/heater/const.py

@@ -1,50 +1,50 @@
-from homeassistant.const import ATTR_TEMPERATURE
 from homeassistant.components.climate.const import (
-    ATTR_HVAC_MODE, ATTR_PRESET_MODE, HVAC_MODE_OFF, HVAC_MODE_HEAT
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    HVAC_MODE_HEAT,
+    HVAC_MODE_OFF,
 )
+from homeassistant.const import ATTR_TEMPERATURE
 
-ATTR_TARGET_TEMPERATURE = 'target_temperature'
-ATTR_CHILD_LOCK = 'child_lock'
-ATTR_ERROR = 'error'
-ATTR_POWER_MODE_AUTO = 'auto'
-ATTR_POWER_MODE_USER = 'user'
-ATTR_POWER_LEVEL = 'power_level'
-ATTR_DISPLAY_ON = 'display_on'
-ATTR_POWER_MODE = 'power_mode'
-ATTR_ECO_TARGET_TEMPERATURE = 'eco_' + ATTR_TARGET_TEMPERATURE
+ATTR_TARGET_TEMPERATURE = "target_temperature"
+ATTR_CHILD_LOCK = "child_lock"
+ATTR_ERROR = "error"
+ATTR_POWER_MODE_AUTO = "auto"
+ATTR_POWER_MODE_USER = "user"
+ATTR_POWER_LEVEL = "power_level"
+ATTR_DISPLAY_ON = "display_on"
+ATTR_POWER_MODE = "power_mode"
+ATTR_ECO_TARGET_TEMPERATURE = "eco_" + ATTR_TARGET_TEMPERATURE
 
-STATE_COMFORT = 'Comfort'
-STATE_ECO = 'Eco'
-STATE_ANTI_FREEZE = 'Anti-freeze'
+STATE_COMFORT = "Comfort"
+STATE_ECO = "Eco"
+STATE_ANTI_FREEZE = "Anti-freeze"
 
 PROPERTY_TO_DPS_ID = {
-    ATTR_HVAC_MODE: '1',
-    ATTR_TARGET_TEMPERATURE: '2',
-    ATTR_TEMPERATURE: '3',
-    ATTR_PRESET_MODE: '4',
-    ATTR_CHILD_LOCK: '6',
-    ATTR_ERROR: '12',
-    ATTR_POWER_LEVEL: '101',
-    ATTR_DISPLAY_ON: '104',
-    ATTR_POWER_MODE: '105',
-    ATTR_ECO_TARGET_TEMPERATURE: '106'
+    ATTR_HVAC_MODE: "1",
+    ATTR_TARGET_TEMPERATURE: "2",
+    ATTR_TEMPERATURE: "3",
+    ATTR_PRESET_MODE: "4",
+    ATTR_CHILD_LOCK: "6",
+    ATTR_ERROR: "12",
+    ATTR_POWER_LEVEL: "101",
+    ATTR_DISPLAY_ON: "104",
+    ATTR_POWER_MODE: "105",
+    ATTR_ECO_TARGET_TEMPERATURE: "106",
 }
 
-HVAC_MODE_TO_DPS_MODE = {
-    HVAC_MODE_OFF: False,
-    HVAC_MODE_HEAT: True
-}
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}
 PRESET_MODE_TO_DPS_MODE = {
-    STATE_COMFORT: 'C',
-    STATE_ECO: 'ECO',
-    STATE_ANTI_FREEZE: 'AF'
+    STATE_COMFORT: "C",
+    STATE_ECO: "ECO",
+    STATE_ANTI_FREEZE: "AF",
 }
 POWER_LEVEL_TO_DPS_LEVEL = {
-    'Stop': 'stop',
-    '1': '1',
-    '2': '2',
-    '3': '3',
-    '4': '4',
-    '5': '5',
-    'Auto': 'auto'
+    "Stop": "stop",
+    "1": "1",
+    "2": "2",
+    "3": "3",
+    "4": "4",
+    "5": "5",
+    "Auto": "auto",
 }

+ 13 - 9
custom_components/goldair_climate/heater/light.py

@@ -1,15 +1,12 @@
 """
 Platform to control the LED display light on Goldair WiFi-connected heaters and panels.
 """
+from homeassistant.components.climate import ATTR_HVAC_MODE, HVAC_MODE_OFF
 from homeassistant.components.light import Light
 from homeassistant.const import STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
-from .const import (
-    ATTR_DISPLAY_ON, PROPERTY_TO_DPS_ID, HVAC_MODE_TO_DPS_MODE
-)
-from homeassistant.components.climate import (
-    ATTR_HVAC_MODE, HVAC_MODE_OFF
-)
+from .const import ATTR_DISPLAY_ON, HVAC_MODE_TO_DPS_MODE, PROPERTY_TO_DPS_ID
 
 
 class GoldairHeaterLedDisplayLight(Light):
@@ -37,7 +34,10 @@ class GoldairHeaterLedDisplayLight(Light):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
         dps_display_on = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON])
 
-        if dps_hvac_mode is None or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]:
+        if (
+            dps_hvac_mode is None
+            or dps_hvac_mode == HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]
+        ):
             return STATE_UNAVAILABLE
         else:
             return dps_display_on
@@ -46,14 +46,18 @@ class GoldairHeaterLedDisplayLight(Light):
         await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], True)
 
     async def async_turn_off(self):
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False
+        )
 
     async def async_toggle(self):
         dps_hvac_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
         dps_display_on = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON])
 
         if dps_hvac_mode != HVAC_MODE_TO_DPS_MODE[HVAC_MODE_OFF]:
-            await (self.async_turn_on() if not dps_display_on else self.async_turn_off())
+            await (
+                self.async_turn_on() if not dps_display_on else self.async_turn_off()
+            )
 
     async def async_update(self):
         await self._device.async_refresh()

+ 6 - 5
custom_components/goldair_climate/heater/lock.py

@@ -1,12 +1,11 @@
 """
 Platform to control the child lock on Goldair WiFi-connected heaters and panels.
 """
-from homeassistant.components.lock import (STATE_LOCKED, STATE_UNLOCKED, LockDevice)
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockDevice
 from homeassistant.const import STATE_UNAVAILABLE
+
 from ..device import GoldairTuyaDevice
-from .const import (
-    ATTR_CHILD_LOCK, PROPERTY_TO_DPS_ID
-)
+from .const import ATTR_CHILD_LOCK, PROPERTY_TO_DPS_ID
 
 
 class GoldairHeaterChildLock(LockDevice):
@@ -47,7 +46,9 @@ class GoldairHeaterChildLock(LockDevice):
 
     async def async_unlock(self, **kwargs):
         """Turn off the child lock."""
-        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], False)
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], False
+        )
 
     async def async_update(self):
         await self._device.async_refresh()

+ 4 - 3
custom_components/goldair_climate/light.py

@@ -2,10 +2,11 @@
 Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
-from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN)
-from .heater.light import GoldairHeaterLedDisplayLight
+from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER)
 from .dehumidifier.light import GoldairDehumidifierLedDisplayLight
 from .fan.light import GoldairFanLedDisplayLight
+from .heater.light import GoldairHeaterLedDisplayLight
 
 
 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -23,6 +24,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
     discovery_info = {
         CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE]
+        CONF_TYPE: config[CONF_TYPE],
     }
     await async_setup_platform(hass, {}, async_add_entities, discovery_info)

+ 5 - 4
custom_components/goldair_climate/lock.py

@@ -2,9 +2,10 @@
 Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
-from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN)
-from .heater.lock import GoldairHeaterChildLock
+from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER)
 from .dehumidifier.lock import GoldairDehumidifierChildLock
+from .heater.lock import GoldairHeaterChildLock
 
 
 async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -15,13 +16,13 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N
     if discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
         async_add_devices([GoldairDehumidifierChildLock(device)])
     if discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
-        raise ValueError('Goldair fains do not support Child Lock.')
+        raise ValueError("Goldair fains do not support Child Lock.")
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):
     config = {**config_entry.data, **config_entry.options}
     discovery_info = {
         CONF_DEVICE_ID: config[CONF_DEVICE_ID],
-        CONF_TYPE: config[CONF_TYPE]
+        CONF_TYPE: config[CONF_TYPE],
     }
     await async_setup_platform(hass, {}, async_add_entities, discovery_info)

+ 0 - 1
custom_components/goldair_climate/manifest.json

@@ -9,6 +9,5 @@
   "requirements": [
     "pytuya>=7.0.5"
   ],
-  "homeassistant": "0.96.0",
   "config_flow": true
 }

+ 1 - 1
custom_components/goldair_climate/strings.json

@@ -1,6 +1,6 @@
 {
+  "title": "Goldair Climate",
   "config": {
-    "title": "Goldair Climate",
     "step": {
       "user": {
         "title": "Configure your Goldair climate device",