Skip to content

Commit

Permalink
Support for climate, select, and switch (#14)
Browse files Browse the repository at this point in the history
Added support for more entities
- Select
- Switch
- Climate with some limitations
  - Not yet support for preset modes (e.g. eco)
  - Not yet support for setting HVAC mode except off/auto

Hide unknown properties by default

Added data dictionary and translation strings for 009-109 (aircondition)
  • Loading branch information
oyvindwe authored Jul 7, 2024
1 parent 856b737 commit 3c3d253
Show file tree
Hide file tree
Showing 16 changed files with 1,037 additions and 86 deletions.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ConnectLife

ConnectLife integration for Home Assistant

## Installation
Expand All @@ -16,17 +17,40 @@ Download the `connectlife` directory and place in your `<config>/custom_componen

After installing, you need to restart Home Assistant.

Finally add "ConnectLife" as an integration in the UI, and provide the username and password for your ConnectLife account.
Finally, add "ConnectLife" as an integration in the UI, and provide the username and password for your ConnectLife account.

You device and all their status values should show up.

## Supported devices

Any unknown device will show up as sensors with names based on their properties. As there are a lot of exposed
sensors, all unknown sensors are hidden by default. Access the device or entity list to view sensors and change
visibility.

You appliances and all their status values should show up.
### Known devices

| Device name | Device type | Device type code | Device feature code | Data dictionary |
|--------------|-----------------|------------------|---------------------|-------------------------------------------------------------------------------------------|
| W-DW50/60-22 | Dishwasher | 015 | 000 | [Most properties completed](custom_components/connectlife/data_dictionaries/015-000.yaml) |
| | Air conditioner | 009 | 109 | [In progress](custom_components/connectlife/data_dictionaries/009-109.yaml) |

Please, please, please contribute PRs with [data dictionaries](custom_components/connectlife/data_dictionaries) for your devices!

## Issues

### Missing data dictionaries
### Climate entities

Please ignore the following warning in the log:
```
Entity None (<class 'custom_components.connectlife.climate.ConnectLifeClimate'>) implements HVACMode(s): auto, off and therefore implicitly supports the turn_on/turn_off methods without setting the proper ClimateEntityFeature. Please report it to the author of the 'connectlife' custom integration
```

Please contribute PR with [data dictionary](custom_components/connectlife/data_dictionaries) for your device!
Missing features:
- Preset modes (e.g. eco)
- Setting HVAC mode except to off/auto
- Min/max temperatures

### Experimental service to set property values
### Experimental service to set property values (sensors only)

Entity service `connectlife.set_value` can be used to set values. Use with caution, as there is **no** validation
if property is writeable, or that the value is legal to set.
Expand Down
2 changes: 1 addition & 1 deletion custom_components/connectlife/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .const import DOMAIN
from .coordinator import ConnectLifeCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR, Platform.SELECT, Platform.SWITCH]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Expand Down
11 changes: 3 additions & 8 deletions custom_components/connectlife/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

Expand Down Expand Up @@ -30,7 +31,7 @@ async def async_setup_entry(
dictionary = Dictionaries.get_dictionary(appliance)
async_add_entities(
ConnectLifeBinaryStatusSensor(coordinator, appliance, s, dictionary[s])
for s in appliance.status_list if hasattr(dictionary[s], "binary_sensor")
for s in appliance.status_list if hasattr(dictionary[s], Platform.BINARY_SENSOR)
)


Expand Down Expand Up @@ -68,11 +69,5 @@ def update_state(self):
self._attr_is_on = VALUES[value]
else:
self._attr_is_on = None
_LOGGER.warning("Unknown value %n for %s", value, self.status)
_LOGGER.warning("Unknown value %d for %s", value, self.status)
self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.update_state()
self.async_write_ha_state()
212 changes: 212 additions & 0 deletions custom_components/connectlife/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""Provides climate entities for ConnectLife."""
import logging

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, Platform, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import (
DOMAIN,
FAN_MODE,
HVAC_ACTION,
IS_ON,
SWING_MODE,
TARGET_TEMPERATURE,
TEMPERATURE_UNIT,
)
from .coordinator import ConnectLifeCoordinator
from .dictionaries import Dictionaries, Property, Climate
from .entity import ConnectLifeEntity
from connectlife.appliance import ConnectLifeAppliance

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ConnectLife sensors."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for appliance in coordinator.appliances.values():
dictionary = Dictionaries.get_dictionary(appliance)
if is_climate(dictionary):
entities.append(ConnectLifeClimate(coordinator, appliance, dictionary))
async_add_entities(entities)


def is_climate(dictionary: dict[str, Property]):
for property in dictionary.values():
if hasattr(property, Platform.CLIMATE):
return True
return False


class ConnectLifeClimate(ConnectLifeEntity, ClimateEntity):
"""Climate class for ConnectLife."""

_attr_has_entity_name = True
_attr_precision = PRECISION_WHOLE
_attr_target_temperature_step = 1
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_mode = None
_attr_fan_mode = None
_attr_swing_mode = None
target_map = {}
fan_mode_map: dict[int, str] = {}
fan_mode_reverse_map: dict[str, int] = {}
hvac_action_map: dict[int, HVACMode] = {}
swing_mode_map: dict[int, str] = {}
swing_mode_reverse_map: dict[str, int] = {}
temperature_unit_map: dict[int, UnitOfTemperature] = {}

def __init__(
self,
coordinator: ConnectLifeCoordinator,
appliance: ConnectLifeAppliance,
data_dictionary: dict[str, Property]
):
"""Initialize the entity."""
super().__init__(coordinator, appliance)
self._attr_unique_id = f"{appliance.device_id}-climate"

self.entity_description = ClimateEntityDescription(
key=self._attr_unique_id,
name=appliance.device_nickname,
translation_key=DOMAIN
)

for dd_entry in data_dictionary.values():
if hasattr(dd_entry, Platform.CLIMATE):
self.target_map[dd_entry.climate.target] = dd_entry.name

hvac_modes = [HVACMode.AUTO]
for target, status in self.target_map.items():
if target == IS_ON:
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
hvac_modes.append(HVACMode.OFF)
if target == TARGET_TEMPERATURE:
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
if target == TEMPERATURE_UNIT:
for k, v in data_dictionary[status].climate.options.items():
if v == "celsius" or v == "C":
self.temperature_unit_map[k] = UnitOfTemperature.CELSIUS
elif v == "fahrenheit" or v == "F":
self.temperature_unit_map[k] = UnitOfTemperature.FAHRENHEIT
if target == FAN_MODE:
self.fan_mode_map = data_dictionary[status].climate.options
self.fan_mode_reverse_map = {v: k for k, v in self.fan_mode_map.items()}
self._attr_fan_modes = list(self.fan_mode_map.values())
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
if target == SWING_MODE:
self.swing_mode_map = data_dictionary[status].climate.options
self.swing_mode_reverse_map = {v: k for k, v in self.swing_mode_map.items()}
self._attr_swing_modes = list(self.swing_mode_map.values())
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
if target == HVAC_ACTION:
# values = set(item.value for item in Fruit)
actions = set(action.value for action in HVACAction)
for (k, v) in data_dictionary[status].climate.options.items():
if v in actions:
self.hvac_action_map[k] = HVACAction(v)
else:
_LOGGER.warning("Not mapping %d to unknown HVACAction %s", k, v)

self._attr_hvac_modes = hvac_modes
self.update_state()

@callback
def update_state(self) -> None:
for target, status in self.target_map.items():
if status in self.coordinator.appliances[self.device_id].status_list:
value = self.coordinator.appliances[self.device_id].status_list[status]
if target == IS_ON:
# TODO: Support value mapping
if value == 0:
self._attr_hvac_mode = HVACMode.OFF
else:
# TODO: Support other modes
self._attr_hvac_mode = HVACMode.AUTO
elif target == HVAC_ACTION:
if value in self.hvac_action_map:
self._attr_hvac_action = self.hvac_action_map[value]
else:
# Map to None as we canot add custom HVAC actions.
self._attr_hvac_action = None
elif target == FAN_MODE:
if value in self.fan_mode_map:
self._attr_fan_mode = self.fan_mode_map[value]
else:
self._attr_fan_mode = None
_LOGGER.warning("Got unexpected value %d for %s", value, status)
elif target == SWING_MODE:
if value in self.swing_mode_map:
self._attr_swing_mode = self.swing_mode_map[value]
else:
self._attr_swing_mode = None
_LOGGER.warning("Got unexpected value %d for %s", value, status)
elif target == TEMPERATURE_UNIT:
if value in self.temperature_unit_map:
self._attr_temperature_unit = self.temperature_unit_map[value]
else:
_LOGGER.warning("Got unexpected value %d for %s", value, status)
else:
setattr(self, f"_attr_{target}", value)
self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1

async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs and TARGET_TEMPERATURE in self.target_map:
target_temperature = round(kwargs[ATTR_TEMPERATURE])
await self.coordinator.api.update_appliance(self.puid, {self.target_map[TARGET_TEMPERATURE]: target_temperature})
self._attr_target_temperature = target_temperature
self.async_write_ha_state()

async def async_turn_on(self):
"""Turn the entity on."""
# TODO: Support value mapping
await self.coordinator.api.update_appliance(self.puid, {self.target_map[IS_ON]: 1})
self.hvac_mode = HVACMode.AUTO
self.async_write_ha_state()

async def async_turn_off(self):
"""Turn the entity off."""
# TODO: Support value mapping
await self.coordinator.api.update_appliance(self.puid, {self.target_map[IS_ON]: 0})
self.hvac_mode = HVACMode.OFF
self.async_write_ha_state()

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
elif hvac_mode == HVACMode.AUTO:
await self.async_turn_on()
# self.async_write_ha_state()

async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
await self.coordinator.api.update_appliance(self.puid, {
self.target_map[FAN_MODE]: self.fan_mode_reverse_map[fan_mode]
})
self._attr_fan_mode = fan_mode
self.async_write_ha_state()

async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode."""
await self.coordinator.api.update_appliance(self.puid, {
self.target_map[SWING_MODE]: self.swing_mode_reverse_map[swing_mode]
})
self._attr_swing_mode = swing_mode
self.async_write_ha_state()
7 changes: 7 additions & 0 deletions custom_components/connectlife/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@
ATTR_DEVICE = "device"
ATTR_DESC = "desc"
ATTR_KEY = "key"

HVAC_ACTION = "hvac_action"
FAN_MODE = "fan_mode"
IS_ON = "is_on"
SWING_MODE = "swing_mode"
TARGET_TEMPERATURE = "target_temperature"
TEMPERATURE_UNIT = "temperature_unit"
73 changes: 73 additions & 0 deletions custom_components/connectlife/data_dictionaries/009-109.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
device_type: aircondition
properties:
- property: f_temp_in
climate:
target: current_temperature
- property: oem_host_version
sensor:
hide: true
- property: t_beep
switch:
- property: t_eco
switch:
- property: t_fan_mute
switch:
- property: t_fan_speed
climate:
target: fan_mode
options:
0: auto
5: low
6: middle_low
7: medium
8: middle_high
9: high
- property: t_humidity
- property: t_power
climate:
target: is_on
- property: t_sleep
- property: t_super
switch:
device_class: switch
- property: t_swing_angle
select:
options:
0: swing
1: auto
2: angle_1
3: angle_2
4: angle_3
5: angle_4
6: angle_5
7: angle_6
- property: t_swing_direction
climate:
target: swing_mode
options:
0: forward
1: right
2: swing
3: both_sides
4: left
- property: t_temp
climate:
target: target_temperature
- property: t_temp_type
climate:
target: temperature_unit
options:
0: celsius
1: fahrenheit
- property: t_tms
switch:
device_class: switch
- property: t_work_mode
climate:
target: hvac_action
options:
0: fan
1: heating
2: cooling
3: drying
# 4: auto - not supported in Home Assistant
Loading

0 comments on commit 3c3d253

Please sign in to comment.