Răsfoiți Sursa

Add support for GPCV and GECO series heaters. (#28)

Add support for GPCV and GECO series heaters.

Based on changes in KiLLeRRat/homeassistant-goldair-climate and the manual for GECO/GPEH heaters.

Co-authored-by: Jason Rumney <jasonrumney@clarion.com.my>
Jason Rumney 5 ani în urmă
părinte
comite
fefa995975

+ 23 - 6
README.md

@@ -4,7 +4,7 @@
 
 The `goldair_climate` 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), and [WiFi-enabled fans](http://www.goldair.co.nz/product-catalogue/cooling/pedestal-fans/40cm-dc-quiet-fan-with-wifi-and-remote-gcpf315) into Home Assistant, enabling control of setting the following parameters via the UI and the following services:
 
-**Heaters**
+**GPPH Heaters**
 
 - **power** (on/off)
 - **mode** (Comfort, Eco, Anti-freeze)
@@ -13,7 +13,20 @@ The `goldair_climate` component integrates [Goldair WiFi-enabled heaters](http:/
 
 Current temperature is also displayed.
 
-**Demudifiers**
+**GPCV Heaters**
+- **power** (on/off)
+- **mode** (Low, High)
+- **target temperature** (`15`-`35` in °C)
+
+Current temperature is also displayed.
+
+**GECO Heaters**
+- **power** (on/off)
+- **target temperature** (`15`-`35` in °C)
+
+Current temperature is also displayed.
+
+**Dehumudifiers**
 
 - **power** (on/off)
 - **mode** (Normal, Low, High, Dry clothes, Air clean)
@@ -42,7 +55,11 @@ There was previously a sensor option, however this is easily achieved using a [t
 
 ### Warning
 
-Please note, this component has currently only been tested with the Goldair GPPH (inverter), GPDH420 (dehumidifier), and GCPF315 fan, however theoretically it should also work with GEPH and GPCV heater devices, may work with the GPDH440 dehumidifier and any other Goldair heaters, dehumidifiers or fans based on the Tuya platform.
+Please note, this component has currently only been tested with the Goldair GPPH (inverter), GPDH420 (dehumidifier), and GCPF315 fan, however theoretically it should also work with GECO, GEPH and GPCV heater devices, may work with the GPDH440 dehumidifier and any other Goldair heaters, dehumidifiers or fans based on the Tuya platform.
+
+GPCV support is based on feedback from etamtlosz on Issue #27
+GECO support is based on work in KiLLeRRaT/homeassistant-goldair-climate and the feature set from the online manual for these heaters. GEPH heaters appear to be the same as the GECO270, so may also work with this setting.  This heater is almost compatible with the GPCV but without the Low/High mode. 
+
 
 ---
 
@@ -87,7 +104,7 @@ goldair_climate:
 
 #### type
 
-&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;_(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`, `gpcv_heater`, `geco_heater`, `dehumidifier` or `fan`.
 
 &nbsp;&nbsp;&nbsp;&nbsp;_Default value: auto_
 
@@ -99,7 +116,7 @@ goldair_climate:
 
 #### display_light
 
-&nbsp;&nbsp;&nbsp;&nbsp;_(boolean) (Optional)_ Whether to surface this appliance's LED display control as a light.
+&nbsp;&nbsp;&nbsp;&nbsp;_(boolean) (Optional)_ Whether to surface this appliance's LED display control as a light (not supported for GPCV or GECO heaters).
 
 &nbsp;&nbsp;&nbsp;&nbsp;_Default value: false_
 
@@ -111,7 +128,7 @@ goldair_climate:
 
 ## Heater gotchas
 
-Goldair 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: you must change the operation mode _before_ setting the new target temperature, otherwise you will set the current thermostat rather than the new one.
+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: you must change the operation mode _before_ setting the new target temperature, otherwise you will set the current thermostat rather than the new one.
 
 When switching to Anti-freeze mode, the heater will set the current power level to `1` as if you had manually chosen it. When you switch back to other modes, you will no longer be in `Auto` and will have to set it again if this is what you wanted. This could be worked around in code however it would require storing state that may be cleared if HA is restarted and due to this unreliability it's probably best that you just factor it into your automations.
 

+ 8 - 0
custom_components/goldair_climate/climate.py

@@ -7,12 +7,16 @@ from .const import (
     CONF_TYPE,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_HEATER,
     CONF_CLIMATE,
     CONF_TYPE_AUTO,
 )
 from .dehumidifier.climate import GoldairDehumidifier
 from .fan.climate import GoldairFan
+from .geco_heater.climate import GoldairGECOHeater
+from .gpcv_heater.climate import GoldairGPCVHeater
 from .heater.climate import GoldairHeater
 
 
@@ -33,6 +37,10 @@ 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_GECO_HEATER:
+        data[CONF_CLIMATE] = GoldairGECOHeater(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
+        data[CONF_CLIMATE] = GoldairGPCVHeater(device)
 
     if CONF_CLIMATE in data:
         async_add_entities([data[CONF_CLIMATE]])

+ 10 - 1
custom_components/goldair_climate/configuration.py

@@ -8,6 +8,8 @@ from .const import (
     CONF_TYPE_HEATER,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
     CONF_CLIMATE,
     CONF_DISPLAY_LIGHT,
     CONF_CHILD_LOCK,
@@ -22,7 +24,14 @@ INDIVIDUAL_CONFIG_SCHEMA_TEMPLATE = [
     {
         "key": CONF_TYPE,
         "type": vol.In(
-            [CONF_TYPE_AUTO, CONF_TYPE_HEATER, CONF_TYPE_DEHUMIDIFIER, CONF_TYPE_FAN]
+            [
+                CONF_TYPE_AUTO,
+                CONF_TYPE_HEATER,
+                CONF_TYPE_DEHUMIDIFIER,
+                CONF_TYPE_FAN,
+                CONF_TYPE_GECO_HEATER,
+                CONF_TYPE_GPCV_HEATER,
+            ]
         ),
         "required": False,
         "default": CONF_TYPE_AUTO,

+ 2 - 0
custom_components/goldair_climate/const.py

@@ -9,6 +9,8 @@ CONF_TYPE_AUTO = "auto"
 CONF_TYPE_HEATER = "heater"
 CONF_TYPE_DEHUMIDIFIER = "dehumidifier"
 CONF_TYPE_FAN = "fan"
+CONF_TYPE_GPCV_HEATER = "gpcv_heater"
+CONF_TYPE_GECO_HEATER = "geco_heater"
 CONF_CLIMATE = "climate"
 CONF_DISPLAY_LIGHT = "display_light"
 CONF_CHILD_LOCK = "child_lock"

+ 7 - 1
custom_components/goldair_climate/device.py

@@ -15,6 +15,8 @@ from .const import (
     API_PROTOCOL_VERSIONS,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_HEATER,
 )
 
@@ -84,12 +86,16 @@ class GoldairTuyaDevice(object):
         cached_state = self._get_cached_state()
 
         _LOGGER.debug(f"Inferring device type from cached state: {cached_state}")
-        if "5" in cached_state:
+        if "5" in cached_state and "3" not 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
+        if "7" in cached_state:
+            return CONF_TYPE_GPCV_HEATER
+        if "3" in cached_state:
+            return CONF_TYPE_GECO_HEATER
 
         return None
 

+ 0 - 0
custom_components/goldair_climate/geco_heater/__init__.py


+ 151 - 0
custom_components/goldair_climate/geco_heater/climate.py

@@ -0,0 +1,151 @@
+"""
+Goldair GECO WiFi Heater device.
+"""
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    HVAC_MODE_HEAT,
+    SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import ATTR_TEMPERATURE, STATE_UNAVAILABLE
+
+from ..device import GoldairTuyaDevice
+from .const import (
+    ATTR_ERROR,
+    ATTR_TARGET_TEMPERATURE,
+    HVAC_MODE_TO_DPS_MODE,
+    PROPERTY_TO_DPS_ID,
+)
+
+SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
+
+
+class GoldairGECOHeater(ClimateDevice):
+    """Representation of a Goldair GECO WiFi heater."""
+
+    def __init__(self, device):
+        """Initialize the heater.
+        Args:
+            device (GoldairTuyaDevice): 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_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 GoldairTuyaDevice.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 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()

+ 21 - 0
custom_components/goldair_climate/geco_heater/const.py

@@ -0,0 +1,21 @@
+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"
+
+PROPERTY_TO_DPS_ID = {
+    ATTR_HVAC_MODE: "1",
+    ATTR_CHILD_LOCK: "2",
+    ATTR_TARGET_TEMPERATURE: "3",
+    ATTR_TEMPERATURE: "4",
+    ATTR_ERROR: "6",
+}
+
+HVAC_MODE_TO_DPS_MODE = {HVAC_MODE_OFF: False, HVAC_MODE_HEAT: True}

+ 64 - 0
custom_components/goldair_climate/geco_heater/lock.py

@@ -0,0 +1,64 @@
+"""
+Platform to control the child lock on Goldair GECO WiFi-connected heaters and panels.
+"""
+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
+
+
+class GoldairGECOHeaterChildLock(LockDevice):
+    """Representation of a Goldair GECO WiFi-connected heater child lock."""
+
+    def __init__(self, device):
+        """Initialize the lock.
+        Args:
+            device (GoldairTuyaDevice): 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()

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


+ 179 - 0
custom_components/goldair_climate/gpcv_heater/climate.py

@@ -0,0 +1,179 @@
+"""
+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 GoldairTuyaDevice
+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 (GoldairTuyaDevice): 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 GoldairTuyaDevice.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 GoldairTuyaDevice.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()

+ 26 - 0
custom_components/goldair_climate/gpcv_heater/const.py

@@ -0,0 +1,26 @@
+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/goldair_climate/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 GoldairTuyaDevice
+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 (GoldairTuyaDevice): 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()

+ 6 - 0
custom_components/goldair_climate/light.py

@@ -7,6 +7,8 @@ from .const import (
     CONF_TYPE,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_HEATER,
     CONF_DISPLAY_LIGHT,
     CONF_TYPE_AUTO,
@@ -33,6 +35,10 @@ 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_GECO_HEATER:
+        raise ValueError("Goldair GECO Heaters do not support panel lighting control.")
 
     if CONF_DISPLAY_LIGHT in data:
         async_add_entities([data[CONF_DISPLAY_LIGHT]])

+ 10 - 2
custom_components/goldair_climate/lock.py

@@ -7,11 +7,15 @@ from .const import (
     CONF_TYPE,
     CONF_TYPE_DEHUMIDIFIER,
     CONF_TYPE_FAN,
+    CONF_TYPE_GECO_HEATER,
+    CONF_TYPE_GPCV_HEATER,
     CONF_TYPE_HEATER,
     CONF_CHILD_LOCK,
     CONF_TYPE_AUTO,
 )
 from .dehumidifier.lock import GoldairDehumidifierChildLock
+from .gpcv_heater.lock import GoldairGPCVHeaterChildLock
+from .geco_heater.lock import GoldairGECOHeaterChildLock
 from .heater.lock import GoldairHeaterChildLock
 import logging
 
@@ -32,10 +36,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:
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_FAN:
         raise ValueError("Goldair fans do not support child lock.")
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_GECO_HEATER:
+        data[CONF_CHILD_LOCK] = GoldairGECOHeaterChildLock(device)
+    elif discovery_info[CONF_TYPE] == CONF_TYPE_GPCV_HEATER:
+        data[CONF_CHILD_LOCK] = GoldairGPCVHeaterChildLock(device)
 
     if CONF_CHILD_LOCK in data:
         async_add_entities([data[CONF_CHILD_LOCK]])