Skip to content

Commit

Permalink
Add the support of Aqara YuBa T1
Browse files Browse the repository at this point in the history
  • Loading branch information
niceboy committed Oct 24, 2023
1 parent 63aa3fb commit 45b0515
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 13 deletions.
123 changes: 116 additions & 7 deletions custom_components/aqara_gateway/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,42 @@
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE
SUPPORT_FAN_MODE,
SUPPORT_SWING_MODE,
SWING_OFF,
SWING_ON
)

from . import DOMAIN, GatewayGenericDevice
from .core.gateway import Gateway
from .core.const import HVAC_MODES, AC_STATE_FAN, AC_STATE_HVAC, FAN_MODES
from .core.const import (
HVAC_MODES,
AC_STATE_FAN,
AC_STATE_HVAC,
FAN_MODES,
YUBA_STATE_HVAC,
YUBA_STATE_FAN
)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Perform the setup for Xiaomi/Aqara devices."""
def setup(gateway: Gateway, device: dict, attr: str):
async_add_entities([
AqaraGenericClimate(gateway, device, attr)
])
if attr == 'yuba':
async_add_entities([
AqaraClimateYuba(gateway, device, attr)])
else:
async_add_entities([
AqaraGenericClimate(gateway, device, attr)
])

aqara_gateway: Gateway = hass.data[DOMAIN][config_entry.entry_id]
aqara_gateway.add_setup('climate', setup)
Expand All @@ -47,6 +63,7 @@ def __init__(self, device, name, attr):
self._current_temp = None
self._fan_mode = None
self._hvac_mode = None
self._swing_mode = None
self._is_on = None
self._state = None
self._target_temp = 0
Expand Down Expand Up @@ -108,9 +125,11 @@ def update(self, data: dict = None):
# https://github.com/AlexxIT/XiaomiGateway3/issues/101#issuecomment-747305596
if self._is_on:
if 'mode' in data: # 0 - heat, 1 - cool, 15 - off
self._hvac_mode = HVAC_MODES[data['mode']]
self._hvac_mode = list(
YUBA_STATE_HVAC.keys())[list(YUBA_STATE_HVAC.values()).index(data['mode'])]
if 'fan_mode' in data: # 0 - low, 3 - auto, 15 - off
self._fan_mode = FAN_MODES[data['fan_mode']]
self._fan_mode = list(
YUBA_STATE_FAN.keys())[list(YUBA_STATE_FAN.values()).index(data['fan_mode'])]
if 'target_temperature' in data: # 255 - off
self._target_temp = data['target_temperature']

Expand Down Expand Up @@ -168,3 +187,93 @@ def set_hvac_mode(self, hvac_mode: str) -> None:
self._state[0] = AC_STATE_HVAC[hvac_mode]
state = int.from_bytes(self._state, 'big')
self.gateway.send(self.device, {self._attr: state})


class AqaraClimateYuba(AqaraGenericClimate, ClimateEntity):
# pylint: disable=too-many-instance-attributes
"""Initialize the AqaraClimateYuba."""

@property
def hvac_modes(self):
""" return hvac modes """
return [HVAC_MODE_OFF, HVAC_MODE_DRY, HVAC_MODE_HEAT, HVAC_MODE_AUTO]

@property
def supported_features(self):
""" return supported features """
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE

@property
def swing_mode(self) -> str | None:
"""Return the swing setting.
Requires ClimateEntityFeature.SWING_MODE.
"""
return self._swing_mode

@property
def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.
Requires ClimateEntityFeature.SWING_MODE.
"""
return [SWING_OFF, SWING_ON]

def set_temperature(self, **kwargs) -> None:
""" set temperature """
if not self._state or kwargs[ATTR_TEMPERATURE] == 0:
self.debug(f"Can't set climate temperature: {self._state}")
return
self.gateway.send(self.device, {self._attr: int(kwargs[ATTR_TEMPERATURE])})

def set_fan_mode(self, fan_mode: str) -> None:
""" set fan mode """
if not self._state:
return
self.gateway.send(self.device, {self._attr: YUBA_STATE_FAN[fan_mode]})

def set_hvac_mode(self, hvac_mode: str) -> None:
""" set hvac mode """
if not self._state:
return
self.gateway.send(self.device, {self._attr: YUBA_STATE_HVAC[hvac_mode]})

def set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation."""
if not self._state:
return
value = 1
if swing_mode == SWING_ON:
value = 0
self.gateway.send(self.device, {self._attr: value})

def update(self, data: dict = None):
# pylint: disable=broad-except
""" update climate """
try:
if 'power' in data: # 0 - off, 1 - on
self._is_on = data['power']

if self._is_on:
if 'mode' in data:
self._hvac_mode = HVAC_MODES[data['mode']]
if 'fan_mode' in data:
self._fan_mode = FAN_MODES[data['fan_mode']]
if 'swing_mode' in data:
self._swing_mode = SWING_OFF if data['swing_mode'] == 1 else SWING_ON
if 'target_temperature' in data:
self._target_temp = data['target_temperature']

else:
self._fan_mode = None
self._hvac_mode = None
self._target_temp = 0

if 'current_temperature' in data:
self._current_temp = data['current_temperature']


except Exception:
_LOGGER.exception("Can't read climate data: %s", data)

self.schedule_update_ha_state()
20 changes: 18 additions & 2 deletions custom_components/aqara_gateway/core/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
# FAN_MIDDLE,
# FAN_OFF,
# FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
# HVAC_MODE_DRY,
# HVAC_MODE_FAN_ONLY,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
# HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF
Expand Down Expand Up @@ -132,6 +133,7 @@
'cover',
'light',
'remote',
'select',
'sensor',
'switch']

Expand Down Expand Up @@ -206,6 +208,20 @@
FAN_AUTO: 0x30
}

YUBA_STATE_HVAC = {
HVAC_MODE_OFF: 0,
HVAC_MODE_HEAT: 0,
HVAC_MODE_DRY: 3,
HVAC_MODE_FAN_ONLY: 4,
HVAC_MODE_AUTO: 5
}

YUBA_STATE_FAN = {
FAN_LOW: 0,
FAN_MEDIUM: 1,
FAN_HIGH: 2
}

# Cover
RUN_STATES = {0: STATE_CLOSING, 1: STATE_OPENING, 2: "stop", 3: "hinder_stop"}

Expand Down
41 changes: 37 additions & 4 deletions custom_components/aqara_gateway/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,7 @@
'params': [
['3.51.85', None, 'occupancy', 'binary_sensor'],
['8.0.2115', None, 'detect_interval', None],
['4.1.85', None, 'monitoring_mode', None],
['4.2.85', None, 'reverted_mode', None],
['4.22.85', None, '4.22.85', None],
['14.47.85', None, 'approaching_distance', None],
['14.48.85', None, '14.48.85', None],
['14.49.85', None, '14.49.85', None],
['14.92.85', None, 'edge_region', None],
Expand All @@ -486,6 +483,9 @@
['14.56.85', None, 'detecting_region', None],
['13.21.85', None, 'occupancy_region', 'sensor'],
['13.27.85', None, 'movements', 'sensor'],
['4.1.85', None, 'monitoring_mode', 'select'],
['4.2.85', None, 'reverted_mode', 'select'],
['14.47.85', None, 'approaching_distance', 'select'],
]
}, {
# water leak sensor
Expand Down Expand Up @@ -896,7 +896,7 @@
'lumi.switch.b2nacn01': ["Aqara", "Double Wall Switch T1", "QBKG20LM"],
'lumi.switch.acn045': ["Aqara", "Double Wall Switch J1", ""],
'lumi.switch.acn049': ["Aqara", "Two-way Control module T2", "ZNQBKG39LM"],
'lumi.switch.acn047': ["Aqara", "Two-way Control module T2", "LLKZMK12LM"],
# 'lumi.switch.acn047': ["Aqara", "Two-way Control module T2", "LLKZMK12LM"],
'params': [
['4.1.85', 'channel_0', 'channel 1', 'switch'],
['4.2.85', 'channel_1', 'channel 2', 'switch'],
Expand Down Expand Up @@ -1280,6 +1280,20 @@
['4.24.85', 'auto_feed', 'auto_feed_switch', 'switch'],
['13.104.85', 'portion', 'portion', 'sensor']
]
}, {
'lumi.bhf_light.acn001': ["Aqara", "Smart Yuba T1", "ZNYB01LM"],
'params': [
['1.7.85', 'light_level', 'brightness', None],
['1.9.85', 'colour_temperature', 'color_temp', None],
['4.1.85', 'power_status', 'light', 'light'],
['4.21.85', 'power_status', 'power', None],
['0.1.85', None, 'current_temperature', None],
['1.8.85', None, 'target_temperature', None],
['14.35.85', None, 'fan_mode', None],
['14.47.85', None, 'swing_mode', None],
['14.51.85', None, 'mode', None],
[None, 'yuba', 'yuba', 'climate'],
]
}]

DEVICES_MIOT = [{
Expand Down Expand Up @@ -1777,6 +1791,25 @@ def get_feature_suppported(zigbee_model: str) -> Optional[bool]:
feature['support_in_use'] = True
return feature

@staticmethod
def get_select_options(zigbee_model: str, attr: str) -> Optional[dict]:
if zigbee_model in ['lumi.bhf_light.acn001']:
if attr == 'fan_mode':
return {"Low": 0, "Middle": 1, "High": 2}
if attr == 'swing_mode':
return {"Enable": 0, "Disable": 1}
if attr == 'operating_mode':
return {"Warm": 0, "Dry": 3, "Fan": 4, "Exhaust": 5}
if zigbee_model in ['lumi.motion.ac01']:
if attr == 'monitoring_mode':
return {"Undirected": 0, "Left and right": 1}
if attr == 'approaching_distance':
return {"Near": 0, "Middle": 1, "Far": 2}
if attr == 'reverted_mode':
return {"Disable": 0, "Enable": 1}

return {"Off": 0, "On": 1}

@staticmethod
def gateway_illuminance_supported(model: str) -> Optional[bool]:
""" return the gateway illuminance supported """
Expand Down
69 changes: 69 additions & 0 deletions custom_components/aqara_gateway/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Support for Aqara Select."""

from homeassistant.core import callback
from homeassistant.components.select import SelectEntity
from homeassistant.helpers.restore_state import RestoreEntity

from . import DOMAIN, GatewayGenericDevice, gateway_state_property
from .core.gateway import Gateway

from .core.utils import Utils


async def async_setup_entry(hass, config_entry, async_add_entities):
""" Perform the setup for Xiaomi/Aqara devices. """
def setup(gateway: Gateway, device: dict, attr: str):
feature = Utils.get_feature_suppported(device["model"])
async_add_entities([
GatewaySelect(gateway, device, attr, feature)
])
aqara_gateway: Gateway = hass.data[DOMAIN][config_entry.entry_id]
aqara_gateway.add_setup('select', setup)


async def async_unload_entry(hass, entry):
# pylint: disable=unused-argument
""" unload entry """
return True


class GatewaySelect(GatewayGenericDevice, SelectEntity, RestoreEntity):
"""Representation of a Xiaomi/Aqara Select."""
# pylint: disable=unused-argument, too-many-instance-attributes
def __init__(
self,
gateway,
device,
attr,
feature
):
"""Initialize."""
self._model = device['model']
self.feature = feature
self._attr_current_option = None
self._attr_options = []
self._attr_state = None
self._map = {}
super().__init__(gateway, device, attr)

@callback
def async_restore_last_state(self, state: str, attrs: dict):
self._attr_current_option = state

async def async_added_to_hass(self) -> None:
"""Restore last state."""
await super().async_added_to_hass()
self._map = Utils.get_select_options(self.device["model"], self._attr)
self._attr_options = list(self._map.keys())

def update(self, data: dict = None):
"""update switch."""
for key, value in data.items():
if key == self._attr:
self._attr_current_option = list(
self._map.keys())[list(self._map.values()).index(data[self._attr])]
self.async_write_ha_state()

async def async_select_option(self, option: str):
""" set select option"""
self.gateway.send(self.device, {self._attr: self._map[option]})

0 comments on commit 45b0515

Please sign in to comment.