Просмотр исходного кода

Merge make-all/homeassistant-goldair-climate master

Adds support for Goldair GPCV Heaters.
Jason Rumney 5 лет назад
Родитель
Сommit
eddcca9a14

+ 12 - 5
README.md

@@ -5,7 +5,7 @@ The `tuya_local` component integrates
 [Goldair WiFi-enabled heaters](http://www.goldair.co.nz/product-catalogue/heating/wifi-heaters), WiFi-enabled [dehumidifiers](http://www.goldair.co.nz/product-catalogue/heating/dehumidifiers), [WiFi-enabled fans](http://www.goldair.co.nz/product-catalogue/cooling/pedestal-fans/40cm-dc-quiet-fan-with-wifi-and-remote-gcpf315) and [Kogan WiFi-enabled heaters](https://www.kogan.com/au/c/smarterhome-range/shop/heating-cooling/) into Home Assistant, enabling control of setting the following parameters via the UI and the following services:
 
 ### Climate devices
-**Goldair Heaters**
+**GPPH Heaters**
 * **power** (on/off)
 * **mode** (Comfort, Eco, Anti-freeze)
 * **target temperature** (`5`-`35` in Comfort mode, `5`-`21` in Eco mode, in °C)
@@ -13,7 +13,14 @@ The `tuya_local` component integrates
 
 Current temperature is also displayed.
 
-**Goldair Demudifiers**
+**Goldair GPCV Heaters**
+* **power** (on/off)
+* **mode** (Low, High)
+* **target temperature** (`15`-`35` in °C)
+
+Current temperature is also displayed.
+
+**Goldair Dehumidifiers**
 * **power** (on/off)
 * **mode** (Normal, Low, High, Dry clothes, Air clean)
 * **target humidity** (`30`-`80`%)
@@ -96,7 +103,7 @@ tuya_local:
                                               [as per the instructions below](#finding-your-device-id-and-local-key).
 
 #### type
-    *(string) (Optional)* The type of Tuya device. `auto` to automatically detect the device type, or if that doesn't work, select from the available options `heater`, `dehumidifier`, `fan` or `kogan_heater`.
+    *(string) (Optional)* The type of Tuya device. `auto` to automatically detect the device type, or if that doesn't work, select from the available options `heater`, `gpcv_heater`, `dehumidifier`, `fan` or `kogan_heater`.
 
     *Default value: auto*
 
@@ -106,7 +113,7 @@ tuya_local:
     *Default value: true* 
 
 #### display_light
-    *(boolean) (Optional)* Whether to surface this appliance's LED display control as a light (not supported for Kogan Heaters).
+    *(boolean) (Optional)* Whether to surface this appliance's LED display control as a light (not supported for Kogan and GPCV Heaters).
 
     *Default value: false* 
 
@@ -117,7 +124,7 @@ tuya_local:
 
 Heater gotchas
 --------------
-Goldair heaters have individual target temperatures for their Comfort and Eco modes, whereas Home Assistant only supports
+Goldair GPPH heaters have individual target temperatures for their Comfort and Eco modes, whereas Home Assistant only supports
 a single target temperature. Therefore, when you're in Comfort mode you will set the Comfort temperature (`5`-`35`), and
 when you're in Eco mode you will set the Eco temperature (`5`-`21`), just like you were using the heater's own control 
 panel. Bear this in mind when writing automations that change the operation mode and set a temperature at the same time: 

+ 5 - 2
custom_components/tuya_local/climate.py

@@ -3,10 +3,11 @@ Setup for different kinds of Tuya climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_TYPE_KOGAN_HEATER,
-                    CONF_CLIMATE, CONF_TYPE_AUTO)
+                    CONF_TYPE_FAN, CONF_TYPE_GPCV_HEATER, CONF_TYPE_HEATER,
+                    CONF_TYPE_KOGAN_HEATER, CONF_CLIMATE, CONF_TYPE_AUTO)
 from .dehumidifier.climate import GoldairDehumidifier
 from .fan.climate import GoldairFan
+from .gpcv_heater.climate import GoldairGPCVHeater
 from .heater.climate import GoldairHeater
 from .kogan_heater.climate import KoganHeater
 
@@ -27,6 +28,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         data[CONF_CLIMATE] = GoldairDehumidifier(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
         data[CONF_CLIMATE] = GoldairFan(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
+        data[CONF_CLIMATE] = GoldairGPCVHeater(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
         data[CONF_CLIMATE] = KoganHeater(device)
 

+ 4 - 2
custom_components/tuya_local/configuration.py

@@ -2,7 +2,9 @@ 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_TYPE_KOGAN_HEATER, CONF_CLIMATE, CONF_DISPLAY_LIGHT, CONF_CHILD_LOCK,
+                    CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN,
+                    CONF_TYPE_GPCV_HEATER, CONF_TYPE_KOGAN_HEATER,
+                    CONF_CLIMATE, CONF_DISPLAY_LIGHT, CONF_CHILD_LOCK,
                     CONF_TYPE_AUTO)
 
 INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
@@ -12,7 +14,7 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
     {"key": CONF_LOCAL_KEY, "type": str, "required": True, "option": True},
     {
         "key": CONF_TYPE,
-        "type": vol.In([CONF_TYPE_AUTO, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_KOGAN_HEATER]),
+        "type": vol.In([CONF_TYPE_AUTO, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_GPCV_HEATER, CONF_TYPE_KOGAN_HEATER]),
         "required": False,
         "default": CONF_TYPE_AUTO,
         "option": True,

+ 1 - 0
custom_components/tuya_local/const.py

@@ -9,6 +9,7 @@ CONF_TYPE_AUTO = "auto"
 CONF_TYPE_HEATER = "heater"
 CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
 CONF_TYPE_FAN = "fan"
+CONF_TYPE_GPCV_HEATER = "gpcv_heater"
 CONF_TYPE_KOGAN_HEATER = "kogan_heater"
 CONF_CLIMATE = "climate"
 CONF_DISPLAY_LIGHT = "display_light"

+ 7 - 2
custom_components/tuya_local/device.py

@@ -9,7 +9,9 @@ from time import time
 
 from homeassistant.const import TEMP_CELSIUS
 
-from .const import DOMAIN, API_PROTOCOL_VERSIONS, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_TYPE_KOGAN_HEATER
+from .const import (
+    DOMAIN, API_PROTOCOL_VERSIONS, CONF_TYPE_DEHUMIDIFIER,CONF_TYPE_FAN,
+    CONF_TYPE_GPCV_HEATER, CONF_TYPE_HEATER, CONF_TYPE_KOGAN_HEATER)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -81,7 +83,10 @@ class TuyaLocalDevice(object):
 
         _LOGGER.debug(f"Inferring device type from cached state: {cached_state}")
         if "5" in cached_state:
-            return CONF_TYPE_DEHUMIDIFIER
+            if "3" in cached_state:
+                return CONF_TYPE_GPCV_HEATER
+            else:
+                return CONF_TYPE_DEHUMIDIFIER
         if "8" in cached_state:
             return CONF_TYPE_FAN
         if "106" in cached_state:

+ 0 - 0
custom_components/tuya_local/gpcv_heater/__init__.py


+ 184 - 0
custom_components/tuya_local/gpcv_heater/climate.py

@@ -0,0 +1,184 @@
+"""
+Goldair GPCV WiFi Heater device.
+"""
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    ATTR_PRESET_MODE,
+    HVAC_MODE_HEAT,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from ..device import TuyaLocalDevice
+from .const import (
+    ATTR_ERROR,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    PRESET_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+)
+
+SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+
+
+class GoldairGPCVHeater(ClimateDevice):
+    """Representation of a Goldair GPCV WiFi heater."""
+
+    def __init__(self, device):
+        """Initialize the heater.
+        Args:
+            device (TuyaLocalDevice): The device API instance."""
+        self._device = device
+
+        self._support_flags = SUPPORT_FLAGS
+
+        self._TEMPERATURE_STEP = 1
+        self._TEMPERATURE_LIMITS = {
+            "min": 15,
+            "max": 35
+        }
+
+    @property
+    def supported_features(self):
+        """Return the list of supported features."""
+        return self._support_flags
+
+    @property
+    def should_poll(self):
+        """Return the polling state."""
+        return True
+
+    @property
+    def name(self):
+        """Return the name of the climate device."""
+        return self._device.name
+
+    @property
+    def unique_id(self):
+        """Return the unique id for this heater."""
+        return self._device.unique_id
+
+    @property
+    def device_info(self):
+        """Return device information about this heater."""
+        return self._device.device_info
+
+    @property
+    def icon(self):
+        """Return the icon to use in the frontend for this device."""
+        hvac_mode = self.hvac_mode
+
+        if hvac_mode == HVAC_MODE_HEAT:
+            return "mdi:radiator"
+        else:
+            return "mdi:radiator-disabled"
+
+    @property
+    def temperature_unit(self):
+        """Return the unit of measurement."""
+        return self._device.temperature_unit
+
+    @property
+    def target_temperature(self):
+        """Return the temperature we try to reach."""
+        return self._device.get_property(
+            PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]
+        )
+
+    @property
+    def target_temperature_step(self):
+        """Return the supported step of target temperature."""
+        return self._TEMPERATURE_STEP
+
+    @property
+    def min_temp(self):
+        """Return the minimum temperature."""
+        return self._TEMPERATURE_LIMITS["min"]
+
+    @property
+    def max_temp(self):
+        """Return the maximum temperature."""
+        return self._TEMPERATURE_LIMITS["max"]
+
+    async def async_set_temperature(self, **kwargs):
+        """Set new target temperatures."""
+        if kwargs.get(ATTR_PRESET_MODE) is not None:
+            await self.async_set_preset_mode(kwargs.get(ATTR_PRESET_MODE))
+        if kwargs.get(ATTR_TEMPERATURE) is not None:
+            await self.async_set_target_temperature(kwargs.get(ATTR_TEMPERATURE))
+
+    async def async_set_target_temperature(self, target_temperature):
+        target_temperature = int(round(target_temperature))
+
+        limits = self._TEMPERATURE_LIMITS
+        if not limits["min"] <= target_temperature <= limits["max"]:
+            raise ValueError(
+                f"Target temperature ({target_temperature}) must be between "
+                f'{limits["min"]} and {limits["max"]}'
+            )
+
+        await self._device.async_set_property(
+            PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE], target_temperature
+        )
+
+    @property
+    def current_temperature(self):
+        """Return the current temperature."""
+        return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE])
+
+    @property
+    def hvac_mode(self):
+        """Return current HVAC mode, ie Heat or Off."""
+        dps_mode = self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE])
+
+        if dps_mode is not None:
+            return TuyaLocalDevice.get_key_for_value(HVAC_MODE_TO_DPS_MODE, dps_mode)
+        else:
+            return STATE_UNAVAILABLE
+
+    @property
+    def hvac_modes(self):
+        """Return the list of available HVAC modes."""
+        return list(HVAC_MODE_TO_DPS_MODE.keys())
+
+    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
+        )
+
+    @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 TuyaLocalDevice.get_key_for_value(
+                PRESET_MODE_TO_DPS_MODE, dps_mode
+            )
+        else:
+            return None
+
+    @property
+    def preset_modes(self):
+        """Return the list of available preset modes."""
+        return list(PRESET_MODE_TO_DPS_MODE.keys())
+
+    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
+        )
+
+    @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])
+
+        return {ATTR_ERROR: error or None}
+
+    async def async_update(self):
+        await self._device.async_refresh()

+ 29 - 0
custom_components/tuya_local/gpcv_heater/const.py

@@ -0,0 +1,29 @@
+from homeassistant.components.climate.const import (
+    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"
+
+PRESET_LOW = "Low"
+PRESET_HIGH = "High"
+
+PROPERTY_TO_DPS_ID = {
+    ATTR_HVAC_MODE: "1",
+    ATTR_CHILD_LOCK: "2",
+    ATTR_TARGET_TEMPERATURE: "3",
+    ATTR_TEMPERATURE: "4",
+    ATTR_ERROR: "6",
+    ATTR_PRESET_MODE: "7",
+}
+
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}
+PRESET_MODE_TO_DPS_MODE = {
+    PRESET_LOW: "Low",
+    PRESET_HIGH: "High"
+}

+ 64 - 0
custom_components/tuya_local/gpcv_heater/lock.py

@@ -0,0 +1,64 @@
+"""
+Platform to control the child lock on Goldair GPCV WiFi-connected heaters and panels.
+"""
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockDevice
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..device import TuyaLocalDevice
+from .const import ATTR_CHILD_LOCK, PROPERTY_TO_DPS_ID
+
+
+class GoldairGPCVHeaterChildLock(LockDevice):
+    """Representation of a Goldair GPCV WiFi-connected heater child lock."""
+
+    def __init__(self, device):
+        """Initialize the lock.
+        Args:
+            device (TuyaLocalDevice): The device API instance."""
+        self._device = device
+
+    @property
+    def should_poll(self):
+        """Return the polling state."""
+        return True
+
+    @property
+    def name(self):
+        """Return the name of the lock."""
+        return self._device.name
+
+    @property
+    def unique_id(self):
+        """Return the unique id for this heater child lock."""
+        return self._device.unique_id
+
+    @property
+    def device_info(self):
+        """Return device information about this heater child lock."""
+        return self._device.device_info
+
+    @property
+    def state(self):
+        """Return the current state."""
+        if self.is_locked is None:
+            return STATE_UNAVAILABLE
+        else:
+            return STATE_LOCKED if self.is_locked else STATE_UNLOCKED
+
+    @property
+    def is_locked(self):
+        """Return the a boolean representing whether the child lock is on or not."""
+        return self._device.get_property(PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK])
+
+    async def async_lock(self, **kwargs):
+        """Turn on the child lock."""
+        await self._device.async_set_property(PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], True)
+
+    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
+        )
+
+    async def async_update(self):
+        await self._device.async_refresh()

+ 4 - 2
custom_components/tuya_local/light.py

@@ -3,8 +3,8 @@ Setup for different kinds of Tuya climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_TYPE_KOGAN_HEATER,
-                    CONF_DISPLAY_LIGHT, CONF_TYPE_AUTO)
+                    CONF_TYPE_FAN, CONF_TYPE_GPCV_HEATER, CONF_TYPE_HEATER,
+                    CONF_TYPE_KOGAN_HEATER, CONF_DISPLAY_LIGHT, CONF_TYPE_AUTO)
 from .dehumidifier.light import GoldairDehumidifierLedDisplayLight
 from .fan.light import GoldairFanLedDisplayLight
 from .heater.light import GoldairHeaterLedDisplayLight
@@ -27,6 +27,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
         data[CONF_DISPLAY_LIGHT] = GoldairDehumidifierLedDisplayLight(device)
     elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
         data[CONF_DISPLAY_LIGHT] = GoldairFanLedDisplayLight(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
+        raise ValueError("Goldair GPCV Heaters do not support panel lighting control.")
     elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
         raise ValueError('Kogan heaters do not support panel lighting control')
 

+ 9 - 5
custom_components/tuya_local/lock.py

@@ -3,8 +3,10 @@ Setup for different kinds of Tuya climate devices
 """
 from . import DOMAIN
 from .const import (CONF_DEVICE_ID, CONF_TYPE, CONF_TYPE_DEHUMIDIFIER,
-                    CONF_TYPE_FAN, CONF_TYPE_HEATER, CONF_TYPE_KOGAN_HEATER, CONF_CHILD_LOCK, CONF_TYPE_AUTO)
+                    CONF_TYPE_FAN, CONF_TYPE_GPCV_HEATER, CONF_TYPE_HEATER,
+                    CONF_TYPE_KOGAN_HEATER, CONF_CHILD_LOCK, CONF_TYPE_AUTO)
 from .dehumidifier.lock import GoldairDehumidifierChildLock
+from .gpcv_heater.lock import GoldairGPCVHeaterChildLock
 from .heater.lock import GoldairHeaterChildLock
 from .kogan_heater.lock import KoganHeaterChildLock
 
@@ -21,12 +23,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
 
     if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER:
         data[CONF_CHILD_LOCK] = GoldairHeaterChildLock(device)
-    if discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_DEHUMIDIFIER:
         data[CONF_CHILD_LOCK] = GoldairDehumidifierChildLock(device)
-    if discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
-        raise ValueError('Goldair fans do not support Child Lock.')
-    if discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
+    elif discovery_info[CONF_TYPE] == CONF_GPCV_HEATER:
+        data[CONF_CHILD_LOCK] = GoldairGPCVHeaterChildLock(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_KOGAN_HEATER:
         data[CONF_CHILD_LOCK] = KoganHeaterChildLock(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
+        raise ValueError("Goldair fans do not support child lock.")
 
     if CONF_CHILD_LOCK in data:
         async_add_entities([data[CONF_CHILD_LOCK]])