Skip to content

Commit

Permalink
Add support for GPCV and GECO series heaters. (#28)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
make-all and jrumney-cm authored May 17, 2020
1 parent 6bb70b7 commit fefa995
Show file tree
Hide file tree
Showing 15 changed files with 571 additions and 10 deletions.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.


---

Expand Down Expand Up @@ -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_

Expand All @@ -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_

Expand All @@ -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.

Expand Down
8 changes: 8 additions & 0 deletions custom_components/goldair_climate/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
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


Expand All @@ -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]])
Expand Down
11 changes: 10 additions & 1 deletion custom_components/goldair_climate/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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,
Expand All @@ -22,7 +24,14 @@
{
"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,
Expand Down
2 changes: 2 additions & 0 deletions custom_components/goldair_climate/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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"
Expand Down
8 changes: 7 additions & 1 deletion custom_components/goldair_climate/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
API_PROTOCOL_VERSIONS,
CONF_TYPE_DEHUMIDIFIER,
CONF_TYPE_FAN,
CONF_TYPE_GECO_HEATER,
CONF_TYPE_GPCV_HEATER,
CONF_TYPE_HEATER,
)

Expand Down Expand Up @@ -84,12 +86,16 @@ async def async_inferred_type(self):
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

Expand Down
Empty file.
151 changes: 151 additions & 0 deletions custom_components/goldair_climate/geco_heater/climate.py
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions custom_components/goldair_climate/geco_heater/const.py
Original file line number Diff line number Diff line change
@@ -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 changes: 64 additions & 0 deletions custom_components/goldair_climate/geco_heater/lock.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file.
Loading

0 comments on commit fefa995

Please sign in to comment.