Răsfoiți Sursa

WIP: Automatically detect device type

Nik Rolls 5 ani în urmă
părinte
comite
39eee9c2e5

+ 4 - 2
README.md

@@ -65,7 +65,6 @@ goldair_climate:
     host: 1.2.3.4
     device_id: <your device id>
     local_key: <your local key>
-    type: heater
 ```
 
 ### Configuration variables
@@ -86,7 +85,10 @@ goldair_climate:
                                               [as per the instructions below](#finding-your-device-id-and-local-key).
 
 #### type
-&nbsp;&nbsp;&nbsp;&nbsp;*(string) (Required)* The type of Goldair device: currently `heater`, `dehumidifier` or `fan`.
+&nbsp;&nbsp;&nbsp;&nbsp;*(string) (Optional)* The type of Goldair device. `auto` to automatically detect the device type, or if that doesn't work, select from the available options `heater`, `dehumidifier` or `fan`.
+
+&nbsp;&nbsp;&nbsp;&nbsp;*Default value: auto*
+
 
 #### climate
 &nbsp;&nbsp;&nbsp;&nbsp;*(boolean) (Optional)* Whether to surface this appliance as a climate device.

+ 3 - 1
custom_components/goldair_climate/__init__.py

@@ -17,7 +17,7 @@ from homeassistant.helpers.discovery import async_load_platform
 from .configuration import individual_config_schema
 from .const import (DOMAIN, 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, SCAN_INTERVAL)
+                    CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_HEATER, SCAN_INTERVAL, CONF_TYPE_AUTO)
 from .device import GoldairTuyaDevice
 
 _LOGGER = logging.getLogger(__name__)
@@ -113,6 +113,8 @@ def setup_device(hass: HomeAssistant, config: dict):
         'device': device
     }
 
+    return device
+
 
 def delete_device(hass: HomeAssistant, config: dict):
     del hass.data[DOMAIN][config[CONF_DEVICE_ID]]['device']

+ 10 - 2
custom_components/goldair_climate/climate.py

@@ -3,7 +3,7 @@ Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_CLIMATE)
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_CLIMATE, CONF_TYPE_AUTO)
 from .dehumidifier.climate import GoldairDehumidifier
 from .fan.climate import GoldairFan
 from .heater.climate import GoldairHeater
@@ -13,6 +13,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Goldair climate device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data['device']
+
+    if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
+        discovery_info[CONF_TYPE] = await device.async_inferred_type()
+
+        if discovery_info[CONF_TYPE] is None:
+            raise ValueError(f"Unable to detect type for device {device.name}")
+
     if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
         data[CONF_CLIMATE] = GoldairHeater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
@@ -20,7 +27,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
         data[CONF_CLIMATE] = GoldairFan(device)
 
-    async_add_entities([data[CONF_CLIMATE]])
+    if CONF_CLIMATE in data:
+        async_add_entities([data[CONF_CLIMATE]])
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 30 - 10
custom_components/goldair_climate/config_flow.py

@@ -1,10 +1,10 @@
 import voluptuous as vol
 from homeassistant import config_entries
-from homeassistant.const import CONF_NAME
-from homeassistant.core import callback
+from homeassistant.const import CONF_NAME, CONF_HOST
+from homeassistant.core import callback, HomeAssistant
 
-from . import DOMAIN, individual_config_schema
-from .const import CONF_DEVICE_ID
+from . import DOMAIN, individual_config_schema, GoldairTuyaDevice
+from .const import CONF_DEVICE_ID, CONF_LOCAL_KEY
 
 
 class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -12,15 +12,22 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
     CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
 
     async def async_step_user(self, user_input=None):
+        errors = {}
+
         if user_input is not None:
             await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
             self._abort_if_unique_id_configured()
-            title = user_input[CONF_NAME]
-            del user_input[CONF_NAME]
-            return self.async_create_entry(title=title, data=user_input)
+
+            connect_success = await async_test_connection(user_input, self.hass)
+            if connect_success:
+                title = user_input[CONF_NAME]
+                del user_input[CONF_NAME]
+                return self.async_create_entry(title=title, data=user_input)
+            else:
+                errors["base"] = "connection"
 
         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(user_input or {})), errors=errors
         )
 
     @staticmethod
@@ -39,13 +46,26 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
 
     async def async_step_user(self, user_input=None):
         """Manage the options."""
+        errors = {}
+        config = {**self.config_entry.data, **self.config_entry.options}
+
         if user_input is not None:
-            return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
+            config = {**config, **user_input}
+            connect_success = await async_test_connection(config, self.hass)
+            if connect_success:
+                return self.async_create_entry(title="", data=user_input)
+            else:
+                errors["base"] = "connection"
 
-        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, options_only=True)
             ),
+            errors=errors
         )
+
+async def async_test_connection(config: dict, hass: HomeAssistant):
+    device = GoldairTuyaDevice("Test", config[CONF_DEVICE_ID], config[CONF_HOST], config[CONF_LOCAL_KEY], hass)
+    await device.async_refresh()
+    return device.get_property("1") is not None

+ 6 - 4
custom_components/goldair_climate/configuration.py

@@ -2,7 +2,8 @@ import voluptuous as vol
 from homeassistant.const import CONF_NAME, CONF_HOST
 
 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)
+                    CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_CLIMATE, CONF_DISPLAY_LIGHT, CONF_CHILD_LOCK,
+                    CONF_TYPE_AUTO)
 
 INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
     {"key": CONF_NAME, "type": str, "required": True, "option": False},
@@ -11,9 +12,10 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
     {"key": CONF_LOCAL_KEY, "type": str, "required": True, "option": True},
     {
         "key": CONF_TYPE,
-        "type": vol.In([CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN]),
-        "required": True,
-        "option": False,
+        "type": vol.In([CONF_TYPE_AUTO, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN]),
+        "required": False,
+        "default": CONF_TYPE_AUTO,
+        "option": True,
     },
     {"key": CONF_CLIMATE, "type": bool, "required": False, "default": True, "option": True},
     {"key": CONF_DISPLAY_LIGHT, "type": bool, "required": False, "default": False, "option": True},

+ 1 - 0
custom_components/goldair_climate/const.py

@@ -5,6 +5,7 @@ DOMAIN = "goldair_climate"
 CONF_DEVICE_ID = "device_id"
 CONF_LOCAL_KEY = "local_key"
 CONF_TYPE = "type"
+CONF_TYPE_AUTO = "auto"
 CONF_TYPE_HEATER = "heater"
 CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
 CONF_TYPE_FAN = "fan"

+ 4 - 1
custom_components/goldair_climate/dehumidifier/climate.py

@@ -18,6 +18,7 @@ from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
 from ..device import GoldairTuyaDevice
 from .const import (
     ATTR_AIR_CLEAN_ON,
+    ATTR_DEFROSTING,
     ATTR_ERROR,
     ATTR_TARGET_HUMIDITY,
     ERROR_CODE_TO_DPS_CODE,
@@ -252,7 +253,9 @@ class GoldairDehumidifier(ClimateDevice):
                 ERROR_CODE_TO_DPS_CODE, error, error
             )
 
-        return {ATTR_ERROR: error or None}
+        defrosting = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_DEFROSTING])
+
+        return {ATTR_ERROR: error or None, ATTR_DEFROSTING: defrosting}
 
     async def async_update(self):
         await self._device.async_refresh()

+ 2 - 0
custom_components/goldair_climate/dehumidifier/const.py

@@ -15,6 +15,7 @@ ATTR_AIR_CLEAN_ON = "air_clean_on"
 ATTR_CHILD_LOCK = "child_lock"
 ATTR_ERROR = "error"
 ATTR_DISPLAY_ON = "display_on"
+ATTR_DEFROSTING = "defrosting"
 
 PRESET_NORMAL = "Normal"
 PRESET_LOW = "Low"
@@ -36,6 +37,7 @@ PROPERTY_TO_DPS_ID = {
     ATTR_DISPLAY_ON: "102",
     ATTR_TEMPERATURE: "103",
     ATTR_HUMIDITY: "104",
+    ATTR_DEFROSTING: "105",
 }
 
 HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_DRY: True}

+ 18 - 1
custom_components/goldair_climate/device.py

@@ -9,7 +9,7 @@ from time import time
 
 from homeassistant.const import TEMP_CELSIUS
 
-from .const import DOMAIN, API_PROTOCOL_VERSIONS
+from .const import DOMAIN, API_PROTOCOL_VERSIONS, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_HEATER
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -69,6 +69,23 @@ class GoldairTuyaDevice(object):
     def temperature_unit(self):
         return self._TEMPERATURE_UNIT
 
+    async def async_inferred_type(self):
+        cached_state = self._get_cached_state()
+
+        if not "1" in cached_state:
+            await self.async_refresh()
+            return await self.async_inferred_type()
+
+        _LOGGER.debug(f"Inferring device type from cached state: {cached_state}")
+        if "5" in cached_state:
+            return CONF_TYPE_DEHUMIDIFIER
+        if "8" in cached_state:
+            return CONF_TYPE_FAN
+        if "106" in cached_state:
+            return CONF_TYPE_HEATER
+
+        return None
+
     def set_fixed_properties(self, fixed_properties):
         self._fixed_properties = fixed_properties
         set_fixed_properties = Timer(

+ 10 - 2
custom_components/goldair_climate/light.py

@@ -3,7 +3,7 @@ Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_DISPLAY_LIGHT)
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_DISPLAY_LIGHT, CONF_TYPE_AUTO)
 from .dehumidifier.light import GoldairDehumidifierLedDisplayLight
 from .fan.light import GoldairFanLedDisplayLight
 from .heater.light import GoldairHeaterLedDisplayLight
@@ -13,6 +13,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Goldair climate device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data['device']
+
+    if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
+        discovery_info[CONF_TYPE] = await device.async_inferred_type()
+
+        if discovery_info[CONF_TYPE] is None:
+            raise ValueError(f"Unable to detect type for device {device.name}")
+
     if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
         data[CONF_DISPLAY_LIGHT] = GoldairHeaterLedDisplayLight(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
@@ -20,7 +27,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
         data[CONF_DISPLAY_LIGHT] = GoldairFanLedDisplayLight(device)
 
-    async_add_entities([data[CONF_DISPLAY_LIGHT]])
+    if CONF_DISPLAY_LIGHT in data:
+        async_add_entities([data[CONF_DISPLAY_LIGHT]])
 
 
 async def async_setup_entry(hass, config_entry, async_add_entities):

+ 8 - 1
custom_components/goldair_climate/lock.py

@@ -3,7 +3,7 @@ Setup for different kinds of Goldair climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_CHILD_LOCK)
+                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_CHILD_LOCK, CONF_TYPE_AUTO)
 from .dehumidifier.lock import GoldairDehumidifierChildLock
 from .heater.lock import GoldairHeaterChildLock
 
@@ -12,6 +12,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
     """Set up the Goldair climate device according to its type."""
     data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
     device = data['device']
+
+    if discovery_info[CONF_TYPE] == CONF_TYPE_AUTO:
+        discovery_info[CONF_TYPE] = await device.async_inferred_type()
+
+        if discovery_info[CONF_TYPE] is None:
+            raise ValueError(f"Unable to detect type for device {device.name}")
+
     if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
         data[CONF_CHILD_LOCK] = GoldairHeaterChildLock(device)
     if discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:

+ 14 - 7
custom_components/goldair_climate/strings.json

@@ -4,21 +4,24 @@
     "step": {
       "user": {
         "title": "Configure your Goldair climate device",
-        "description": "[Follow these instructions to find your local key.](https://github.com/nikrolls/homeassistant-goldair-climate#finding-your-device-id-and-local-key)",
+        "description": "[Follow these instructions to find your device id and local key.](https://github.com/nikrolls/homeassistant-goldair-climate#finding-your-device-id-and-local-key)",
         "data": {
           "name": "Name",
           "host": "IP address or hostname",
           "device_id": "Device ID (uuid)",
           "local_key": "Local key",
           "type": "Device type",
-          "climate": "Add a climate entity",
-          "display_light": "Add LED display as a light entity",
-          "child_lock": "Add child lock as a lock entity"
+          "climate": "Include a climate entity",
+          "display_light": "Include LED display as a light entity",
+          "child_lock": "Include child lock as a lock entity (unsupported on fans)"
         }
       }
     },
     "abort": {
       "already_configured": "A device with that ID has already been added."
+    },
+    "error": {
+      "connection": "Unable to connect to your device with those details. It could be an intermittent issue, or they may be incorrect."
     }
   },
   "options": {
@@ -29,11 +32,15 @@
         "data": {
           "host": "IP address or hostname",
           "local_key": "Local key",
-          "climate": "Add a climate entity",
-          "display_light": "Add LED display as light entity",
-          "child_lock": "Add child lock as lock entity"
+          "type": "Device type",
+          "climate": "Include device as a climate entity",
+          "display_light": "Include LED display as light entity",
+          "child_lock": "Include child lock as lock entity (unsupported on fans)"
         }
       }
+    },
+    "error": {
+      "connection": "Unable to connect to your device with those details. It could be an intermittent issue, or they may be incorrect."
     }
   }
 }