Merge branch 'master' of https://github.com/rospogrigio/localtuya into rospogrigio-master
This commit is contained in:
@@ -53,6 +53,20 @@ localtuya:
|
||||
current: 18 # Optional
|
||||
current_consumption: 19 # Optional
|
||||
voltage: 20 # Optional
|
||||
|
||||
- platform: vacuum
|
||||
friendly_name: Vacuum
|
||||
id: 28
|
||||
idle_status_value: "standby,sleep"
|
||||
returning_status_value: "docking"
|
||||
docked_status_value: "charging,chargecompleted"
|
||||
battery_dp: 14
|
||||
mode_dp: 27
|
||||
modes: "smart,standby,chargego,wall_follow,spiral,single"
|
||||
fan_speed_dp: 30
|
||||
fan_speeds: "low,normal,high"
|
||||
clean_time_dp: 33
|
||||
clean_area_dp: 32
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
394
custom_components/localtuya/climate.py
Normal file
394
custom_components/localtuya/climate.py
Normal file
@@ -0,0 +1,394 @@
|
||||
"""Platform to locally control Tuya-based climate devices."""
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.climate import (
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_TEMP,
|
||||
DOMAIN,
|
||||
ClimateEntity,
|
||||
)
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
CURRENT_HVAC_IDLE,
|
||||
CURRENT_HVAC_HEAT,
|
||||
PRESET_NONE,
|
||||
PRESET_ECO,
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_TEMPERATURE_UNIT,
|
||||
PRECISION_HALVES,
|
||||
PRECISION_TENTHS,
|
||||
PRECISION_WHOLE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
from .const import (
|
||||
CONF_CURRENT_TEMPERATURE_DP,
|
||||
CONF_MAX_TEMP_DP,
|
||||
CONF_MIN_TEMP_DP,
|
||||
CONF_PRECISION,
|
||||
CONF_TARGET_PRECISION,
|
||||
CONF_TARGET_TEMPERATURE_DP,
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_HVAC_MODE_DP,
|
||||
CONF_HVAC_MODE_SET,
|
||||
CONF_HEURISTIC_ACTION,
|
||||
CONF_HVAC_ACTION_DP,
|
||||
CONF_HVAC_ACTION_SET,
|
||||
CONF_ECO_DP,
|
||||
CONF_ECO_VALUE,
|
||||
CONF_PRESET_DP,
|
||||
CONF_PRESET_SET,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HVAC_MODE_SETS = {
|
||||
"manual/auto": {
|
||||
HVAC_MODE_HEAT: "manual",
|
||||
HVAC_MODE_AUTO: "auto",
|
||||
},
|
||||
"Manual/Auto": {
|
||||
HVAC_MODE_HEAT: "Manual",
|
||||
HVAC_MODE_AUTO: "Auto",
|
||||
},
|
||||
"Manual/Program": {
|
||||
HVAC_MODE_HEAT: "Manual",
|
||||
HVAC_MODE_AUTO: "Program",
|
||||
},
|
||||
"True/False": {
|
||||
HVAC_MODE_HEAT: True,
|
||||
},
|
||||
}
|
||||
HVAC_ACTION_SETS = {
|
||||
"True/False": {
|
||||
CURRENT_HVAC_HEAT: True,
|
||||
CURRENT_HVAC_IDLE: False,
|
||||
},
|
||||
"open/close": {
|
||||
CURRENT_HVAC_HEAT: "open",
|
||||
CURRENT_HVAC_IDLE: "close",
|
||||
},
|
||||
"heating/no_heating": {
|
||||
CURRENT_HVAC_HEAT: "heating",
|
||||
CURRENT_HVAC_IDLE: "no_heating",
|
||||
},
|
||||
"Heat/Warming": {
|
||||
CURRENT_HVAC_HEAT: "Heat",
|
||||
CURRENT_HVAC_IDLE: "Warming",
|
||||
},
|
||||
}
|
||||
PRESET_SETS = {
|
||||
"Manual/Holiday/Program": {
|
||||
PRESET_AWAY: "Holiday",
|
||||
PRESET_HOME: "Program",
|
||||
PRESET_NONE: "Manual",
|
||||
},
|
||||
}
|
||||
|
||||
TEMPERATURE_CELSIUS = "celsius"
|
||||
TEMPERATURE_FAHRENHEIT = "fahrenheit"
|
||||
DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS
|
||||
DEFAULT_PRECISION = PRECISION_TENTHS
|
||||
DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES
|
||||
# Empirically tested to work for AVATTO thermostat
|
||||
MODE_WAIT = 0.1
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
"""Return schema used in config flow."""
|
||||
return {
|
||||
vol.Optional(CONF_TARGET_TEMPERATURE_DP): vol.In(dps),
|
||||
vol.Optional(CONF_CURRENT_TEMPERATURE_DP): vol.In(dps),
|
||||
vol.Optional(CONF_TEMPERATURE_STEP): vol.In(
|
||||
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
|
||||
),
|
||||
vol.Optional(CONF_MAX_TEMP_DP): vol.In(dps),
|
||||
vol.Optional(CONF_MIN_TEMP_DP): vol.In(dps),
|
||||
vol.Optional(CONF_PRECISION): vol.In(
|
||||
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
|
||||
),
|
||||
vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps),
|
||||
vol.Optional(CONF_HVAC_MODE_SET): vol.In(list(HVAC_MODE_SETS.keys())),
|
||||
vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps),
|
||||
vol.Optional(CONF_HVAC_ACTION_SET): vol.In(list(HVAC_ACTION_SETS.keys())),
|
||||
vol.Optional(CONF_ECO_DP): vol.In(dps),
|
||||
vol.Optional(CONF_ECO_VALUE): str,
|
||||
vol.Optional(CONF_PRESET_DP): vol.In(dps),
|
||||
vol.Optional(CONF_PRESET_SET): vol.In(list(PRESET_SETS.keys())),
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(
|
||||
[TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT]
|
||||
),
|
||||
vol.Optional(CONF_TARGET_PRECISION): vol.In(
|
||||
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
|
||||
),
|
||||
vol.Optional(CONF_HEURISTIC_ACTION): bool,
|
||||
}
|
||||
|
||||
|
||||
class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity):
|
||||
"""Tuya climate device."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
config_entry,
|
||||
switchid,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize a new LocaltuyaClimate."""
|
||||
super().__init__(device, config_entry, switchid, _LOGGER, **kwargs)
|
||||
self._state = None
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._hvac_mode = None
|
||||
self._preset_mode = None
|
||||
self._hvac_action = None
|
||||
self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION)
|
||||
self._target_precision = self._config.get(
|
||||
CONF_TARGET_PRECISION, self._precision
|
||||
)
|
||||
self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP)
|
||||
self._conf_hvac_mode_set = HVAC_MODE_SETS.get(
|
||||
self._config.get(CONF_HVAC_MODE_SET), {}
|
||||
)
|
||||
self._conf_preset_dp = self._config.get(CONF_PRESET_DP)
|
||||
self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {})
|
||||
self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP)
|
||||
self._conf_hvac_action_set = HVAC_ACTION_SETS.get(
|
||||
self._config.get(CONF_HVAC_ACTION_SET), {}
|
||||
)
|
||||
self._conf_eco_dp = self._config.get(CONF_ECO_DP)
|
||||
self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO")
|
||||
self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(
|
||||
CONF_PRESET_DP
|
||||
)
|
||||
print("Initialized climate [{}]".format(self.name))
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = 0
|
||||
if self.has_config(CONF_TARGET_TEMPERATURE_DP):
|
||||
supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE
|
||||
if self.has_config(CONF_MAX_TEMP_DP):
|
||||
supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||
if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP):
|
||||
supported_features = supported_features | SUPPORT_PRESET_MODE
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
return self._precision
|
||||
|
||||
@property
|
||||
def target_precision(self):
|
||||
"""Return the precision of the target."""
|
||||
return self._target_precision
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
if (
|
||||
self._config.get(CONF_TEMPERATURE_UNIT, DEFAULT_TEMPERATURE_UNIT)
|
||||
== TEMPERATURE_FAHRENHEIT
|
||||
):
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
if not self.has_config(CONF_HVAC_MODE_DP):
|
||||
return None
|
||||
return list(self._conf_hvac_mode_set) + [HVAC_MODE_OFF]
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
"""Return the current running hvac operation if supported.
|
||||
|
||||
Need to be one of CURRENT_HVAC_*.
|
||||
"""
|
||||
if self._config.get(CONF_HEURISTIC_ACTION, False):
|
||||
if self._hvac_mode == HVAC_MODE_HEAT:
|
||||
if self._current_temperature < (
|
||||
self._target_temperature - self._precision
|
||||
):
|
||||
self._hvac_action = CURRENT_HVAC_HEAT
|
||||
if self._current_temperature == (
|
||||
self._target_temperature - self._precision
|
||||
):
|
||||
if self._hvac_action == CURRENT_HVAC_HEAT:
|
||||
self._hvac_action = CURRENT_HVAC_HEAT
|
||||
if self._hvac_action == CURRENT_HVAC_IDLE:
|
||||
self._hvac_action = CURRENT_HVAC_IDLE
|
||||
if (
|
||||
self._current_temperature + self._precision
|
||||
) > self._target_temperature:
|
||||
self._hvac_action = CURRENT_HVAC_IDLE
|
||||
return self._hvac_action
|
||||
return self._hvac_action
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return current preset."""
|
||||
return self._preset_mode
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return the list of available presets modes."""
|
||||
if not self._has_presets:
|
||||
return None
|
||||
presets = list(self._conf_preset_set)
|
||||
if self._conf_eco_dp:
|
||||
presets.append(PRESET_ECO)
|
||||
return presets
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._config.get(CONF_TEMPERATURE_STEP, DEFAULT_TEMPERATURE_STEP)
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return NotImplementedError()
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return NotImplementedError()
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_TEMPERATURE in kwargs and self.has_config(CONF_TARGET_TEMPERATURE_DP):
|
||||
temperature = round(kwargs[ATTR_TEMPERATURE] / self._target_precision)
|
||||
await self._device.set_dp(
|
||||
temperature, self._config[CONF_TARGET_TEMPERATURE_DP]
|
||||
)
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
return NotImplementedError()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target operation mode."""
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
return
|
||||
if not self._state and self._conf_hvac_mode_dp != self._dp_id:
|
||||
await self._device.set_dp(True, self._dp_id)
|
||||
# Some thermostats need a small wait before sending another update
|
||||
await asyncio.sleep(MODE_WAIT)
|
||||
await self._device.set_dp(
|
||||
self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._device.set_dp(True, self._dp_id)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set new target preset mode."""
|
||||
if preset_mode == PRESET_ECO:
|
||||
await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp)
|
||||
return
|
||||
await self._device.set_dp(
|
||||
self._conf_preset_set[preset_mode], self._conf_preset_dp
|
||||
)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self.has_config(CONF_MIN_TEMP_DP):
|
||||
return self.dps_conf(CONF_MIN_TEMP_DP)
|
||||
return DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self.has_config(CONF_MAX_TEMP_DP):
|
||||
return self.dps_conf(CONF_MAX_TEMP_DP)
|
||||
return DEFAULT_MAX_TEMP
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
self._state = self.dps(self._dp_id)
|
||||
|
||||
if self.has_config(CONF_TARGET_TEMPERATURE_DP):
|
||||
self._target_temperature = (
|
||||
self.dps_conf(CONF_TARGET_TEMPERATURE_DP) * self._target_precision
|
||||
)
|
||||
|
||||
if self.has_config(CONF_CURRENT_TEMPERATURE_DP):
|
||||
self._current_temperature = (
|
||||
self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision
|
||||
)
|
||||
|
||||
if self._has_presets:
|
||||
if (
|
||||
self.has_config(CONF_ECO_DP)
|
||||
and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value
|
||||
):
|
||||
self._preset_mode = PRESET_ECO
|
||||
else:
|
||||
for preset, value in self._conf_preset_set.items(): # todo remove
|
||||
if self.dps_conf(CONF_PRESET_DP) == value:
|
||||
self._preset_mode = preset
|
||||
break
|
||||
else:
|
||||
self._preset_mode = PRESET_NONE
|
||||
|
||||
# Update the HVAC status
|
||||
if self.has_config(CONF_HVAC_MODE_DP):
|
||||
if not self._state:
|
||||
self._hvac_mode = HVAC_MODE_OFF
|
||||
else:
|
||||
for mode, value in self._conf_hvac_mode_set.items():
|
||||
if self.dps_conf(CONF_HVAC_MODE_DP) == value:
|
||||
self._hvac_mode = mode
|
||||
break
|
||||
else:
|
||||
# in case hvac mode and preset share the same dp
|
||||
self._hvac_mode = HVAC_MODE_AUTO
|
||||
|
||||
# Update the current action
|
||||
for action, value in self._conf_hvac_action_set.items():
|
||||
if self.dps_conf(CONF_HVAC_ACTION_DP) == value:
|
||||
self._hvac_action = action
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema)
|
@@ -1,6 +1,7 @@
|
||||
"""Code shared between all platforms."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
@@ -9,8 +10,10 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
@@ -117,6 +120,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._is_closing = False
|
||||
self._connect_task = None
|
||||
self._disconnect_task = None
|
||||
self._unsub_interval = None
|
||||
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
|
||||
|
||||
# This has to be done in case the device type is type_0d
|
||||
@@ -166,6 +170,16 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._disconnect_task = async_dispatcher_connect(
|
||||
self._hass, signal, _new_entity_handler
|
||||
)
|
||||
|
||||
if (
|
||||
CONF_SCAN_INTERVAL in self._config_entry
|
||||
and self._config_entry[CONF_SCAN_INTERVAL] > 0
|
||||
):
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
self._async_refresh,
|
||||
timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]),
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
|
||||
if self._interface is not None:
|
||||
@@ -173,6 +187,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._interface = None
|
||||
self._connect_task = None
|
||||
|
||||
async def _async_refresh(self, _now):
|
||||
if self._interface is not None:
|
||||
await self._interface.update_dps()
|
||||
|
||||
async def close(self):
|
||||
"""Close connection and stop re-connect loop."""
|
||||
self._is_closing = True
|
||||
@@ -223,7 +241,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
"""Device disconnected."""
|
||||
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
|
||||
async_dispatcher_send(self._hass, signal, None)
|
||||
|
||||
if self._unsub_interval is not None:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
self._interface = None
|
||||
self.debug("Disconnected - waiting for discovery broadcast")
|
||||
|
||||
@@ -253,13 +273,13 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
|
||||
def _update_handler(status):
|
||||
"""Update entity state when status was updated."""
|
||||
if status is not None:
|
||||
self._status = status
|
||||
self.status_updated()
|
||||
else:
|
||||
self._status = {}
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
if status is None:
|
||||
status = {}
|
||||
if self._status != status:
|
||||
self._status = status.copy()
|
||||
if status:
|
||||
self.status_updated()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}"
|
||||
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
@@ -44,6 +45,7 @@ BASIC_INFO_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_DEVICE_ID): str,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +57,7 @@ DEVICE_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_LOCAL_KEY): cv.string,
|
||||
vol.Required(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,6 +93,7 @@ def options_schema(entities):
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_LOCAL_KEY): str,
|
||||
vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL): int,
|
||||
vol.Required(
|
||||
CONF_ENTITIES, description={"suggested_value": entity_names}
|
||||
): cv.multi_select(entity_names),
|
||||
|
@@ -16,6 +16,7 @@ CONF_COLOR = "color"
|
||||
CONF_COLOR_MODE = "color_mode"
|
||||
CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin"
|
||||
CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin"
|
||||
CONF_COLOR_TEMP_REVERSE = "color_temp_reverse"
|
||||
CONF_MUSIC_MODE = "music_mode"
|
||||
|
||||
# switch
|
||||
@@ -34,13 +35,53 @@ CONF_SPAN_TIME = "span_time"
|
||||
# fan
|
||||
CONF_FAN_SPEED_CONTROL = "fan_speed_control"
|
||||
CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control"
|
||||
CONF_FAN_SPEED_LOW = "fan_speed_low"
|
||||
CONF_FAN_SPEED_MEDIUM = "fan_speed_medium"
|
||||
CONF_FAN_SPEED_HIGH = "fan_speed_high"
|
||||
CONF_FAN_SPEED_MIN = "fan_speed_min"
|
||||
CONF_FAN_SPEED_MAX = "fan_speed_max"
|
||||
CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list"
|
||||
CONF_FAN_DIRECTION = "fan_direction"
|
||||
CONF_FAN_DIRECTION_FWD = "fan_direction_forward"
|
||||
CONF_FAN_DIRECTION_REV = "fan_direction_reverse"
|
||||
|
||||
# sensor
|
||||
CONF_SCALING = "scaling"
|
||||
|
||||
# climate
|
||||
CONF_TARGET_TEMPERATURE_DP = "target_temperature_dp"
|
||||
CONF_CURRENT_TEMPERATURE_DP = "current_temperature_dp"
|
||||
CONF_TEMPERATURE_STEP = "temperature_step"
|
||||
CONF_MAX_TEMP_DP = "max_temperature_dp"
|
||||
CONF_MIN_TEMP_DP = "min_temperature_dp"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_TARGET_PRECISION = "target_precision"
|
||||
CONF_HVAC_MODE_DP = "hvac_mode_dp"
|
||||
CONF_HVAC_MODE_SET = "hvac_mode_set"
|
||||
CONF_PRESET_DP = "preset_dp"
|
||||
CONF_PRESET_SET = "preset_set"
|
||||
CONF_HEURISTIC_ACTION = "heuristic_action"
|
||||
CONF_HVAC_ACTION_DP = "hvac_action_dp"
|
||||
CONF_HVAC_ACTION_SET = "hvac_action_set"
|
||||
CONF_ECO_DP = "eco_dp"
|
||||
CONF_ECO_VALUE = "eco_value"
|
||||
|
||||
# vacuum
|
||||
CONF_POWERGO_DP = "powergo_dp"
|
||||
CONF_IDLE_STATUS_VALUE = "idle_status_value"
|
||||
CONF_RETURNING_STATUS_VALUE = "returning_status_value"
|
||||
CONF_DOCKED_STATUS_VALUE = "docked_status_value"
|
||||
CONF_BATTERY_DP = "battery_dp"
|
||||
CONF_MODE_DP = "mode_dp"
|
||||
CONF_MODES = "modes"
|
||||
CONF_FAN_SPEED_DP = "fan_speed_dp"
|
||||
CONF_FAN_SPEEDS = "fan_speeds"
|
||||
CONF_CLEAN_TIME_DP = "clean_time_dp"
|
||||
CONF_CLEAN_AREA_DP = "clean_area_dp"
|
||||
CONF_CLEAN_RECORD_DP = "clean_record_dp"
|
||||
CONF_LOCATE_DP = "locate_dp"
|
||||
CONF_FAULT_DP = "fault_dp"
|
||||
CONF_PAUSED_STATE = "paused_state"
|
||||
CONF_RETURN_MODE = "return_mode"
|
||||
CONF_STOP_STATUS = "stop_status"
|
||||
|
||||
DATA_DISCOVERY = "discovery"
|
||||
|
||||
DOMAIN = "localtuya"
|
||||
@@ -48,6 +89,7 @@ DOMAIN = "localtuya"
|
||||
# Platforms in this list must support config flows
|
||||
PLATFORMS = [
|
||||
"binary_sensor",
|
||||
"climate",
|
||||
"cover",
|
||||
"fan",
|
||||
"light",
|
||||
@@ -55,6 +97,7 @@ PLATFORMS = [
|
||||
"select",
|
||||
"sensor",
|
||||
"switch",
|
||||
"vacuum",
|
||||
]
|
||||
|
||||
TUYA_DEVICE = "tuya_device"
|
||||
|
@@ -1,26 +1,37 @@
|
||||
"""Platform to locally control Tuya-based fan devices."""
|
||||
import logging
|
||||
import math
|
||||
from functools import partial
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
DIRECTION_REVERSE,
|
||||
DOMAIN,
|
||||
SPEED_HIGH,
|
||||
SPEED_LOW,
|
||||
SPEED_MEDIUM,
|
||||
SPEED_OFF,
|
||||
SUPPORT_DIRECTION,
|
||||
SUPPORT_OSCILLATE,
|
||||
SUPPORT_SET_SPEED,
|
||||
FanEntity,
|
||||
)
|
||||
from homeassistant.util.percentage import (
|
||||
int_states_in_range,
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
from .const import (
|
||||
CONF_FAN_DIRECTION,
|
||||
CONF_FAN_DIRECTION_FWD,
|
||||
CONF_FAN_DIRECTION_REV,
|
||||
CONF_FAN_ORDERED_LIST,
|
||||
CONF_FAN_OSCILLATING_CONTROL,
|
||||
CONF_FAN_SPEED_CONTROL,
|
||||
CONF_FAN_SPEED_HIGH,
|
||||
CONF_FAN_SPEED_LOW,
|
||||
CONF_FAN_SPEED_MEDIUM,
|
||||
CONF_FAN_SPEED_MAX,
|
||||
CONF_FAN_SPEED_MIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -31,15 +42,12 @@ def flow_schema(dps):
|
||||
return {
|
||||
vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps),
|
||||
vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps),
|
||||
vol.Optional(CONF_FAN_SPEED_LOW, default=SPEED_LOW): vol.In(
|
||||
[SPEED_LOW, "1", "2", "small"]
|
||||
),
|
||||
vol.Optional(CONF_FAN_SPEED_MEDIUM, default=SPEED_MEDIUM): vol.In(
|
||||
[SPEED_MEDIUM, "mid", "2", "3"]
|
||||
),
|
||||
vol.Optional(CONF_FAN_SPEED_HIGH, default=SPEED_HIGH): vol.In(
|
||||
[SPEED_HIGH, "auto", "3", "4", "large", "big"]
|
||||
),
|
||||
vol.Optional(CONF_FAN_DIRECTION): vol.In(dps),
|
||||
vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string,
|
||||
vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string,
|
||||
vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int,
|
||||
vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int,
|
||||
vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string,
|
||||
}
|
||||
|
||||
|
||||
@@ -56,28 +64,46 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device, config_entry, fanid, _LOGGER, **kwargs)
|
||||
self._is_on = False
|
||||
self._speed = None
|
||||
self._oscillating = None
|
||||
self._direction = None
|
||||
self._percentage = None
|
||||
self._speed_range = (
|
||||
self._config.get(CONF_FAN_SPEED_MIN),
|
||||
self._config.get(CONF_FAN_SPEED_MAX),
|
||||
)
|
||||
self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",")
|
||||
self._ordered_list_mode = None
|
||||
|
||||
if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1:
|
||||
self._use_ordered_list = True
|
||||
_LOGGER.debug(
|
||||
"Fan _use_ordered_list: %s > %s",
|
||||
self._use_ordered_list,
|
||||
self._ordered_list,
|
||||
)
|
||||
else:
|
||||
self._use_ordered_list = False
|
||||
_LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list)
|
||||
|
||||
@property
|
||||
def oscillating(self):
|
||||
"""Return current oscillating status."""
|
||||
return self._oscillating
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
"""Return the current direction of the fan."""
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if Tuya fan is on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._speed
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
def percentage(self):
|
||||
"""Return the current percentage."""
|
||||
return self._percentage
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
@@ -87,76 +113,143 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Turn on the entity."""
|
||||
_LOGGER.debug("Fan async_turn_on")
|
||||
await self._device.set_dp(True, self._dp_id)
|
||||
if speed is not None:
|
||||
await self.async_set_speed(speed)
|
||||
if percentage is not None:
|
||||
await self.async_set_percentage(percentage)
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the entity."""
|
||||
_LOGGER.debug("Fan async_turn_off")
|
||||
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
async def async_set_percentage(self, percentage):
|
||||
"""Set the speed of the fan."""
|
||||
mapping = {
|
||||
SPEED_LOW: self._config.get(CONF_FAN_SPEED_LOW),
|
||||
SPEED_MEDIUM: self._config.get(CONF_FAN_SPEED_MEDIUM),
|
||||
SPEED_HIGH: self._config.get(CONF_FAN_SPEED_HIGH),
|
||||
}
|
||||
_LOGGER.debug("Fan async_set_percentage: %s", percentage)
|
||||
|
||||
if speed == SPEED_OFF:
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
else:
|
||||
await self._device.set_dp(
|
||||
mapping.get(speed), self._config.get(CONF_FAN_SPEED_CONTROL)
|
||||
)
|
||||
if percentage is not None:
|
||||
if percentage == 0:
|
||||
return await self.async_turn_off()
|
||||
if not self.is_on:
|
||||
await self.async_turn_on()
|
||||
if self._use_ordered_list:
|
||||
await self._device.set_dp(
|
||||
str(
|
||||
percentage_to_ordered_list_item(self._ordered_list, percentage)
|
||||
),
|
||||
self._config.get(CONF_FAN_SPEED_CONTROL),
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Fan async_set_percentage: %s > %s",
|
||||
percentage,
|
||||
percentage_to_ordered_list_item(self._ordered_list, percentage),
|
||||
)
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
else:
|
||||
await self._device.set_dp(
|
||||
str(
|
||||
math.ceil(
|
||||
percentage_to_ranged_value(self._speed_range, percentage)
|
||||
)
|
||||
),
|
||||
self._config.get(CONF_FAN_SPEED_CONTROL),
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Fan async_set_percentage: %s > %s",
|
||||
percentage,
|
||||
percentage_to_ranged_value(self._speed_range, percentage),
|
||||
)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def async_oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation."""
|
||||
_LOGGER.debug("Fan async_oscillate: %s", oscillating)
|
||||
await self._device.set_dp(
|
||||
oscillating, self._config.get(CONF_FAN_OSCILLATING_CONTROL)
|
||||
)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def async_set_direction(self, direction):
|
||||
"""Set the direction of the fan."""
|
||||
_LOGGER.debug("Fan async_set_direction: %s", direction)
|
||||
|
||||
if direction == DIRECTION_FORWARD:
|
||||
value = self._config.get(CONF_FAN_DIRECTION_FWD)
|
||||
|
||||
if direction == DIRECTION_REVERSE:
|
||||
value = self._config.get(CONF_FAN_DIRECTION_REV)
|
||||
await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION))
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
supports = 0
|
||||
features = 0
|
||||
|
||||
if self.has_config(CONF_FAN_OSCILLATING_CONTROL):
|
||||
supports |= SUPPORT_OSCILLATE
|
||||
if self.has_config(CONF_FAN_SPEED_CONTROL):
|
||||
supports |= SUPPORT_SET_SPEED
|
||||
features |= SUPPORT_OSCILLATE
|
||||
|
||||
return supports
|
||||
if self.has_config(CONF_FAN_SPEED_CONTROL):
|
||||
features |= SUPPORT_SET_SPEED
|
||||
|
||||
if self.has_config(CONF_FAN_DIRECTION):
|
||||
features |= SUPPORT_DIRECTION
|
||||
|
||||
return features
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Speed count for the fan."""
|
||||
speed_count = int_states_in_range(self._speed_range)
|
||||
_LOGGER.debug("Fan speed_count: %s", speed_count)
|
||||
return speed_count
|
||||
|
||||
def status_updated(self):
|
||||
"""Get state of Tuya fan."""
|
||||
mappings = {
|
||||
self._config.get(CONF_FAN_SPEED_LOW): SPEED_LOW,
|
||||
self._config.get(CONF_FAN_SPEED_MEDIUM): SPEED_MEDIUM,
|
||||
self._config.get(CONF_FAN_SPEED_HIGH): SPEED_HIGH,
|
||||
}
|
||||
|
||||
self._is_on = self.dps(self._dp_id)
|
||||
|
||||
if self.has_config(CONF_FAN_SPEED_CONTROL):
|
||||
self._speed = mappings.get(self.dps_conf(CONF_FAN_SPEED_CONTROL))
|
||||
if self.speed is None:
|
||||
self.warning(
|
||||
"%s/%s: Ignoring unknown fan controller state: %s",
|
||||
self.name,
|
||||
self.entity_id,
|
||||
self.dps_conf(CONF_FAN_SPEED_CONTROL),
|
||||
current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL)
|
||||
if self._use_ordered_list:
|
||||
_LOGGER.debug(
|
||||
"Fan current_speed ordered_list_item_to_percentage: %s from %s",
|
||||
current_speed,
|
||||
self._ordered_list,
|
||||
)
|
||||
if current_speed is not None:
|
||||
self._percentage = ordered_list_item_to_percentage(
|
||||
self._ordered_list, current_speed
|
||||
)
|
||||
self._speed = None
|
||||
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Fan current_speed ranged_value_to_percentage: %s from %s",
|
||||
current_speed,
|
||||
self._speed_range,
|
||||
)
|
||||
if current_speed is not None:
|
||||
self._percentage = ranged_value_to_percentage(
|
||||
self._speed_range, int(current_speed)
|
||||
)
|
||||
|
||||
_LOGGER.debug("Fan current_percentage: %s", self._percentage)
|
||||
|
||||
if self.has_config(CONF_FAN_OSCILLATING_CONTROL):
|
||||
self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL)
|
||||
_LOGGER.debug("Fan current_oscillating : %s", self._oscillating)
|
||||
|
||||
if self.has_config(CONF_FAN_DIRECTION):
|
||||
value = self.dps_conf(CONF_FAN_DIRECTION)
|
||||
if value is not None:
|
||||
if value == self._config.get(CONF_FAN_DIRECTION_FWD):
|
||||
self._direction = DIRECTION_FORWARD
|
||||
|
||||
if value == self._config.get(CONF_FAN_DIRECTION_REV):
|
||||
self._direction = DIRECTION_REVERSE
|
||||
_LOGGER.debug("Fan current_direction : %s > %s", value, self._direction)
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema)
|
||||
|
@@ -27,15 +27,17 @@ from .const import (
|
||||
CONF_COLOR_MODE,
|
||||
CONF_COLOR_TEMP_MAX_KELVIN,
|
||||
CONF_COLOR_TEMP_MIN_KELVIN,
|
||||
CONF_COLOR_TEMP_REVERSE,
|
||||
CONF_MUSIC_MODE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIRED_TO_KELVIN_CONST = 1000000
|
||||
DEFAULT_MIN_KELVIN = 2700 # MIRED 370
|
||||
DEFAULT_MAX_KELVIN = 6500 # MIRED 153
|
||||
|
||||
DEFAULT_COLOR_TEMP_REVERSE = False
|
||||
|
||||
DEFAULT_LOWER_BRIGHTNESS = 29
|
||||
DEFAULT_UPPER_BRIGHTNESS = 1000
|
||||
|
||||
@@ -117,6 +119,11 @@ def flow_schema(dps):
|
||||
vol.Optional(CONF_COLOR_TEMP_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1500, max=8000)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_COLOR_TEMP_REVERSE,
|
||||
default=DEFAULT_COLOR_TEMP_REVERSE,
|
||||
description={"suggested_value": DEFAULT_COLOR_TEMP_REVERSE},
|
||||
): bool,
|
||||
vol.Optional(CONF_SCENE): vol.In(dps),
|
||||
vol.Optional(
|
||||
CONF_MUSIC_MODE, default=False, description={"suggested_value": False}
|
||||
@@ -146,13 +153,14 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity):
|
||||
CONF_BRIGHTNESS_UPPER, DEFAULT_UPPER_BRIGHTNESS
|
||||
)
|
||||
self._upper_color_temp = self._upper_brightness
|
||||
self._max_mired = round(
|
||||
MIRED_TO_KELVIN_CONST
|
||||
/ self._config.get(CONF_COLOR_TEMP_MIN_KELVIN, DEFAULT_MIN_KELVIN)
|
||||
self._max_mired = color_util.color_temperature_kelvin_to_mired(
|
||||
self._config.get(CONF_COLOR_TEMP_MIN_KELVIN, DEFAULT_MIN_KELVIN)
|
||||
)
|
||||
self._min_mired = round(
|
||||
MIRED_TO_KELVIN_CONST
|
||||
/ self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN)
|
||||
self._min_mired = color_util.color_temperature_kelvin_to_mired(
|
||||
self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN)
|
||||
)
|
||||
self._color_temp_reverse = self._config.get(
|
||||
CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE
|
||||
)
|
||||
self._hs = None
|
||||
self._effect = None
|
||||
@@ -199,11 +207,16 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity):
|
||||
def color_temp(self):
|
||||
"""Return the color_temp of the light."""
|
||||
if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode:
|
||||
color_temp_value = (
|
||||
self._upper_color_temp - self._color_temp
|
||||
if self._color_temp_reverse
|
||||
else self._color_temp
|
||||
)
|
||||
return int(
|
||||
self._max_mired
|
||||
- (
|
||||
((self._max_mired - self._min_mired) / self._upper_color_temp)
|
||||
* self._color_temp
|
||||
* color_temp_value
|
||||
)
|
||||
)
|
||||
return None
|
||||
@@ -364,10 +377,17 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity):
|
||||
if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP):
|
||||
if brightness is None:
|
||||
brightness = self._brightness
|
||||
mired = int(kwargs[ATTR_COLOR_TEMP])
|
||||
if self._color_temp_reverse:
|
||||
mired = self._max_mired - (mired - self._min_mired)
|
||||
if mired < self._min_mired:
|
||||
mired = self._min_mired
|
||||
elif mired > self._max_mired:
|
||||
mired = self._max_mired
|
||||
color_temp = int(
|
||||
self._upper_color_temp
|
||||
- (self._upper_color_temp / (self._max_mired - self._min_mired))
|
||||
* (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired)
|
||||
* (mired - self._min_mired)
|
||||
)
|
||||
states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE
|
||||
states[self._config.get(CONF_BRIGHTNESS)] = brightness
|
||||
|
@@ -4,10 +4,7 @@ from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.number import DOMAIN, NumberEntity
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
|
@@ -21,6 +21,7 @@ Functions
|
||||
json = status() # returns json payload
|
||||
set_version(version) # 3.1 [default] or 3.3
|
||||
detect_available_dps() # returns a list of available dps provided by the device
|
||||
update_dps(dps) # sends update dps command
|
||||
add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the
|
||||
# device (to be queried in the payload)
|
||||
set_dp(on, dp_index) # Set value of any dps index.
|
||||
@@ -61,6 +62,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc")
|
||||
SET = "set"
|
||||
STATUS = "status"
|
||||
HEARTBEAT = "heartbeat"
|
||||
UPDATEDPS = "updatedps" # Request refresh of DPS
|
||||
|
||||
PROTOCOL_VERSION_BYTES_31 = b"3.1"
|
||||
PROTOCOL_VERSION_BYTES_33 = b"3.3"
|
||||
@@ -76,6 +78,9 @@ SUFFIX_VALUE = 0x0000AA55
|
||||
|
||||
HEARTBEAT_INTERVAL = 10
|
||||
|
||||
# DPS that are known to be safe to use with update_dps (0x12) command
|
||||
UPDATE_DPS_WHITELIST = [18, 19, 20] # Socket (Wi-Fi)
|
||||
|
||||
# This is intended to match requests.json payload at
|
||||
# https://github.com/codetheweb/tuyapi :
|
||||
# type_0a devices require the 0a command as the status request
|
||||
@@ -90,11 +95,13 @@ PAYLOAD_DICT = {
|
||||
STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}},
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
},
|
||||
"type_0d": {
|
||||
STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -292,6 +299,8 @@ class MessageDispatcher(ContextualLogger):
|
||||
sem = self.listeners[self.HEARTBEAT_SEQNO]
|
||||
self.listeners[self.HEARTBEAT_SEQNO] = msg
|
||||
sem.release()
|
||||
elif msg.cmd == 0x12:
|
||||
self.debug("Got normal updatedps response")
|
||||
elif msg.cmd == 0x08:
|
||||
self.debug("Got status update")
|
||||
self.listener(msg)
|
||||
@@ -478,6 +487,26 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
"""Send a heartbeat message."""
|
||||
return await self.exchange(HEARTBEAT)
|
||||
|
||||
async def update_dps(self, dps=None):
|
||||
"""
|
||||
Request device to update index.
|
||||
|
||||
Args:
|
||||
dps([int]): list of dps to update, default=detected&whitelisted
|
||||
"""
|
||||
if self.version == 3.3:
|
||||
if dps is None:
|
||||
if not self.dps_cache:
|
||||
await self.detect_available_dps()
|
||||
if self.dps_cache:
|
||||
dps = [int(dp) for dp in self.dps_cache]
|
||||
# filter non whitelisted dps
|
||||
dps = list(set(dps).intersection(set(UPDATE_DPS_WHITELIST)))
|
||||
self.debug("updatedps() entry (dps %s, dps_cache %s)", dps, self.dps_cache)
|
||||
payload = self._generate_payload(UPDATEDPS, dps)
|
||||
self.transport.write(payload)
|
||||
return True
|
||||
|
||||
async def set_dp(self, value, dp_index):
|
||||
"""
|
||||
Set value (may be any type: bool, int or string) of any dps index.
|
||||
@@ -582,7 +611,10 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
json_data["t"] = str(int(time.time()))
|
||||
|
||||
if data is not None:
|
||||
json_data["dps"] = data
|
||||
if "dpId" in json_data:
|
||||
json_data["dpId"] = data
|
||||
else:
|
||||
json_data["dps"] = data
|
||||
elif command_hb == 0x0D:
|
||||
json_data["dps"] = self.dps_to_request
|
||||
|
||||
@@ -591,7 +623,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
|
||||
if self.version == 3.3:
|
||||
payload = self.cipher.encrypt(payload, False)
|
||||
if command_hb != 0x0A:
|
||||
if command_hb not in [0x0A, 0x12]:
|
||||
# add the 3.3 header
|
||||
payload = PROTOCOL_33_HEADER + payload
|
||||
elif command == SET:
|
||||
|
@@ -4,10 +4,7 @@ from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.select import DOMAIN, SelectEntity
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
|
@@ -20,6 +20,7 @@
|
||||
"device_id": "Device ID",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
||||
"device_type": "Device type"
|
||||
}
|
||||
},
|
||||
@@ -39,4 +40,4 @@
|
||||
}
|
||||
},
|
||||
"title": "LocalTuya"
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return device state attributes."""
|
||||
attrs = {}
|
||||
if self.has_config(CONF_CURRENT):
|
||||
|
@@ -29,7 +29,8 @@
|
||||
"host": "Host",
|
||||
"device_id": "Device ID",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version"
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)"
|
||||
}
|
||||
},
|
||||
"pick_entity_type": {
|
||||
@@ -60,25 +61,61 @@
|
||||
"scaling": "Scaling Factor",
|
||||
"state_on": "On Value",
|
||||
"state_off": "Off Value",
|
||||
"powergo_dp": "Power DP (Usually 25 or 2)",
|
||||
"idle_status_value": "Idle Status (comma-separated)",
|
||||
"returning_status_value": "Returning Status",
|
||||
"docked_status_value": "Docked Status (comma-separated)",
|
||||
"fault_dp": "Fault DP (Usually 11)",
|
||||
"battery_dp": "Battery status DP (Usually 14)",
|
||||
"mode_dp": "Mode DP (Usually 27)",
|
||||
"modes": "Modes list",
|
||||
"return_mode": "Return home mode",
|
||||
"fan_speed_dp": "Fan speeds DP (Usually 30)",
|
||||
"fan_speeds": "Fan speeds list (comma-separated)",
|
||||
"clean_time_dp": "Clean Time DP (Usually 33)",
|
||||
"clean_area_dp": "Clean Area DP (Usually 32)",
|
||||
"clean_record_dp": "Clean Record DP (Usually 34)",
|
||||
"locate_dp": "Locate DP (Usually 31)",
|
||||
"paused_state": "Pause state (pause, paused, etc)",
|
||||
"stop_status": "Stop status",
|
||||
"brightness": "Brightness (only for white color)",
|
||||
"brightness_lower": "Brightness Lower Value",
|
||||
"brightness_upper": "Brightness Upper Value",
|
||||
"color_temp": "Color Temperature",
|
||||
"color_temp_reverse": "Color Temperature Reverse",
|
||||
"color": "Color",
|
||||
"color_mode": "Color Mode",
|
||||
"color_temp_min_kelvin": "Minimum Color Temperature in K",
|
||||
"color_temp_max_kelvin": "Maximum Color Temperature in K",
|
||||
"music_mode": "Music mode available",
|
||||
"scene": "Scene",
|
||||
"fan_speed_control": "Fan Speed Control",
|
||||
"fan_oscillating_control": "Fan Oscillating Control",
|
||||
"fan_speed_low": "Fan Low Speed Setting",
|
||||
"fan_speed_medium": "Fan Medium Speed Setting",
|
||||
"fan_speed_high": "Fan High Speed Setting",
|
||||
"max_value": "Maximum Value",
|
||||
"min_value": "Minimum Value",
|
||||
"select_options": "Valid entries, separate entries by a ;",
|
||||
"select_options_friendly": "User Friendly options, separate entries by a ;"
|
||||
"select_options_friendly": "User Friendly options, separate entries by a ;",
|
||||
"fan_speed_control": "Fan Speed Control dps",
|
||||
"fan_oscillating_control": "Fan Oscillating Control dps",
|
||||
"fan_speed_min": "minimum fan speed integer",
|
||||
"fan_speed_max": "maximum fan speed integer",
|
||||
"fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)",
|
||||
"fan_direction":"fan direction dps",
|
||||
"fan_direction_forward": "forward dps string",
|
||||
"fan_direction_reverse": "reverse dps string",
|
||||
"current_temperature_dp": "Current Temperature",
|
||||
"target_temperature_dp": "Target Temperature",
|
||||
"temperature_step": "Temperature Step (optional)",
|
||||
"max_temperature_dp": "Max Temperature (optional)",
|
||||
"min_temperature_dp": "Min Temperature (optional)",
|
||||
"precision": "Precision (optional, for DPs values)",
|
||||
"target_precision": "Target Precision (optional, for DPs values)",
|
||||
"temperature_unit": "Temperature Unit (optional)",
|
||||
"hvac_mode_dp": "HVAC Mode DP (optional)",
|
||||
"hvac_mode_set": "HVAC Mode Set (optional)",
|
||||
"hvac_action_dp": "HVAC Current Action DP (optional)",
|
||||
"hvac_action_set": "HVAC Current Action Set (optional)",
|
||||
"preset_dp": "Presets DP (optional)",
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +130,7 @@
|
||||
"host": "Host",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Protocol Version",
|
||||
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
|
||||
"entities": "Entities (uncheck an entity to remove it)"
|
||||
}
|
||||
},
|
||||
@@ -116,25 +154,61 @@
|
||||
"scaling": "Scaling Factor",
|
||||
"state_on": "On Value",
|
||||
"state_off": "Off Value",
|
||||
"powergo_dp": "Power DP (Usually 25 or 2)",
|
||||
"idle_status_value": "Idle Status (comma-separated)",
|
||||
"returning_status_value": "Returning Status",
|
||||
"docked_status_value": "Docked Status (comma-separated)",
|
||||
"fault_dp": "Fault DP (Usually 11)",
|
||||
"battery_dp": "Battery status DP (Usually 14)",
|
||||
"mode_dp": "Mode DP (Usually 27)",
|
||||
"modes": "Modes list",
|
||||
"return_mode": "Return home mode",
|
||||
"fan_speed_dp": "Fan speeds DP (Usually 30)",
|
||||
"fan_speeds": "Fan speeds list (comma-separated)",
|
||||
"clean_time_dp": "Clean Time DP (Usually 33)",
|
||||
"clean_area_dp": "Clean Area DP (Usually 32)",
|
||||
"clean_record_dp": "Clean Record DP (Usually 34)",
|
||||
"locate_dp": "Locate DP (Usually 31)",
|
||||
"paused_state": "Pause state (pause, paused, etc)",
|
||||
"stop_status": "Stop status",
|
||||
"brightness": "Brightness (only for white color)",
|
||||
"brightness_lower": "Brightness Lower Value",
|
||||
"brightness_upper": "Brightness Upper Value",
|
||||
"color_temp": "Color Temperature",
|
||||
"color_temp_reverse": "Color Temperature Reverse",
|
||||
"color": "Color",
|
||||
"color_mode": "Color Mode",
|
||||
"color_temp_min_kelvin": "Minimum Color Temperature in K",
|
||||
"color_temp_max_kelvin": "Maximum Color Temperature in K",
|
||||
"music_mode": "Music mode available",
|
||||
"scene": "Scene",
|
||||
"fan_speed_control": "Fan Speed Control",
|
||||
"fan_oscillating_control": "Fan Oscillating Control",
|
||||
"fan_speed_low": "Fan Low Speed Setting",
|
||||
"fan_speed_medium": "Fan Medium Speed Setting",
|
||||
"fan_speed_high": "Fan High Speed Setting",
|
||||
"max_value": "Maximum Value",
|
||||
"min_value": "Minimum Value",
|
||||
"select_options": "Valid entries, separate entries by a ;",
|
||||
"select_options_friendly": "User Friendly options, separate entries by a ;"
|
||||
"select_options_friendly": "User Friendly options, separate entries by a ;",
|
||||
"fan_speed_control": "Fan Speed Control dps",
|
||||
"fan_oscillating_control": "Fan Oscillating Control dps",
|
||||
"fan_speed_min": "minimum fan speed integer",
|
||||
"fan_speed_max": "maximum fan speed integer",
|
||||
"fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)",
|
||||
"fan_direction":"fan direction dps",
|
||||
"fan_direction_forward": "forward dps string",
|
||||
"fan_direction_reverse": "reverse dps string",
|
||||
"current_temperature_dp": "Current Temperature",
|
||||
"target_temperature_dp": "Target Temperature",
|
||||
"temperature_step": "Temperature Step (optional)",
|
||||
"max_temperature_dp": "Max Temperature (optional)",
|
||||
"min_temperature_dp": "Min Temperature (optional)",
|
||||
"precision": "Precision (optional, for DPs values)",
|
||||
"target_precision": "Target Precision (optional, for DPs values)",
|
||||
"temperature_unit": "Temperature Unit (optional)",
|
||||
"hvac_mode_dp": "HVAC Mode DP (optional)",
|
||||
"hvac_mode_set": "HVAC Mode Set (optional)",
|
||||
"hvac_action_dp": "HVAC Current Action DP (optional)",
|
||||
"hvac_action_set": "HVAC Current Action Set (optional)",
|
||||
"preset_dp": "Presets DP (optional)",
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
}
|
||||
},
|
||||
"yaml_import": {
|
||||
|
217
custom_components/localtuya/translations/pt-BR.json
Normal file
217
custom_components/localtuya/translations/pt-BR.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "O dispositivo já foi configurado.",
|
||||
"device_updated": "A configuração do dispositivo foi atualizada!"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Não é possível se conectar ao dispositivo. Verifique se o endereço está correto e tente novamente.",
|
||||
"invalid_auth": "Falha ao autenticar com o dispositivo. Verifique se o ID do dispositivo e a chave local estão corretos.",
|
||||
"unknown": "Ocorreu um erro desconhecido. Consulte o registro para obter detalhes.",
|
||||
"entity_already_configured": "A entidade com este ID já foi configurada.",
|
||||
"address_in_use": "O endereço usado para descoberta já está em uso. Certifique-se de que nenhum outro aplicativo o esteja usando (porta TCP 6668).",
|
||||
"discovery_failed": "Algo falhou ao descobrir dispositivos. Consulte o registro para obter detalhes.",
|
||||
"empty_dps": "A conexão com o dispositivo foi bem-sucedida, mas nenhum ponto de dados foi encontrado. Tente novamente. Crie um novo issue e inclua os logs de depuração se o problema persistir."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Descoberta de dispositivo",
|
||||
"description": "Escolha um dos dispositivos descobertos automaticamente ou clique em `...` para adicionar um dispositivo manualmente.",
|
||||
"data": {
|
||||
"discovered_device": "Dispositivo descoberto"
|
||||
}
|
||||
},
|
||||
"basic_info": {
|
||||
"title": "Adicionar dispositivo Tuya",
|
||||
"description": "Preencha os detalhes básicos do dispositivo. O nome inserido aqui será usado para identificar a própria integração (como visto na página `Integrations`). Você adicionará entidades e dará nomes a elas nas etapas a seguir.",
|
||||
"data": {
|
||||
"friendly_name": "Nome",
|
||||
"host": "Host",
|
||||
"device_id": "ID do dispositivo",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Versão do protocolo",
|
||||
"scan_interval": "Intervalo do escaneamento (segundos, somente quando não estiver atualizando automaticamente)"
|
||||
}
|
||||
},
|
||||
"pick_entity_type": {
|
||||
"title": "Seleção do tipo de entidade",
|
||||
"description": "Escolha o tipo de entidade que deseja adicionar.",
|
||||
"data": {
|
||||
"platform_to_add": "Platforma",
|
||||
"no_additional_platforms": "Não adicione mais entidades"
|
||||
}
|
||||
},
|
||||
"add_entity": {
|
||||
"title": "Adicionar nova entidade",
|
||||
"description": "Por favor, preencha os detalhes de uma entidade com o tipo `{platform}`. Todas as configurações, exceto `ID`, podem ser alteradas na página Opções posteriormente.",
|
||||
"data": {
|
||||
"id": "ID",
|
||||
"friendly_name": "Name fantasia",
|
||||
"current": "Atual",
|
||||
"current_consumption": "Consumo atual",
|
||||
"voltage": "Voltagem",
|
||||
"commands_set": "Conjunto de comandos Open_Close_Stop",
|
||||
"positioning_mode": "Modo de posicão",
|
||||
"current_position_dp": "Posição atual (somente para o modo de posição)",
|
||||
"set_position_dp": "Definir posição (somente para o modo de posição)",
|
||||
"position_inverted": "Inverter posição 0-100 (somente para o modo de posição)",
|
||||
"span_time": "Tempo de abertura completo, em segundos. (somente para o modo temporizado)",
|
||||
"unit_of_measurement": "Unidade de medida",
|
||||
"device_class": "Classe do dispositivo",
|
||||
"scaling": "Fator de escala",
|
||||
"state_on": "Valor On",
|
||||
"state_off": "Valor Off",
|
||||
"powergo_dp": "Potência DP (Geralmente 25 ou 2)",
|
||||
"idle_status_value": "Status ocioso (separado por vírgula)",
|
||||
"returning_status_value": "Status de retorno",
|
||||
"docked_status_value": "Status docked (separado por vírgula)",
|
||||
"fault_dp": "Falha DP (Geralmente 11)",
|
||||
"battery_dp": "Status da bateria DP (normalmente 14)",
|
||||
"mode_dp": "Modo DP (Geralmente 27)",
|
||||
"modes": "Lista de modos",
|
||||
"return_mode": "Modo de retorno para base",
|
||||
"fan_speed_dp": "Velocidades do ventilador DP (normalmente 30)",
|
||||
"fan_speeds": "Lista de velocidades do ventilador (separadas por vírgulas)",
|
||||
"clean_time_dp": "Tempo de Limpeza DP (Geralmente 33)",
|
||||
"clean_area_dp": "Área Limpa DP (Geralmente 32)",
|
||||
"clean_record_dp": "Limpar Registro DP (Geralmente 34)",
|
||||
"locate_dp": "Localize DP (Geralmente 31)",
|
||||
"paused_state": "Estado de pausa (pausa, pausado, etc)",
|
||||
"stop_status": "Status de parada",
|
||||
"brightness": "Brilho (somente para cor branca)",
|
||||
"brightness_lower": "Valor mais baixo do brilho",
|
||||
"brightness_upper": "Valor mais alto do brilho",
|
||||
"color_temp": "Temperatura da cor",
|
||||
"color_temp_reverse": "Temperatura da cor reversa",
|
||||
"color": "Cor",
|
||||
"color_mode": "Modo de cor",
|
||||
"color_temp_min_kelvin": "Minima temperatura de cor em K",
|
||||
"color_temp_max_kelvin": "Máxima temperatura de cor em K",
|
||||
"music_mode": "Modo de música disponível",
|
||||
"scene": "Cena",
|
||||
"fan_speed_control": "dps de controle de velocidade do ventilador",
|
||||
"fan_oscillating_control": "dps de controle oscilante do ventilador",
|
||||
"fan_speed_min": "velocidade mínima do ventilador inteiro",
|
||||
"fan_speed_max": "velocidade máxima do ventilador inteiro",
|
||||
"fan_speed_ordered_list": "Lista de modos de velocidade do ventilador (substitui a velocidade min/max)",
|
||||
"fan_direction":"direção do ventilador dps",
|
||||
"fan_direction_forward": "string de dps para frente",
|
||||
"fan_direction_reverse": "string dps reversa",
|
||||
"current_temperature_dp": "Temperatura atual",
|
||||
"target_temperature_dp": "Temperatura alvo",
|
||||
"temperature_step": "Etapa de temperatura (opcional)",
|
||||
"max_temperature_dp": "Max Temperatura (opcional)",
|
||||
"min_temperature_dp": "Min Temperatura (opcional)",
|
||||
"precision": "Precisão (opcional, para valores de DPs)",
|
||||
"target_precision": "Precisão do alvo (opcional, para valores de DPs)",
|
||||
"temperature_unit": "Unidade de Temperatura (opcional)",
|
||||
"hvac_mode_dp": "Modo HVAC DP (opcional)",
|
||||
"hvac_mode_set": "Conjunto de modo HVAC (opcional)",
|
||||
"hvac_action_dp": "Ação atual de HVAC DP (opcional)",
|
||||
"hvac_action_set": "Conjunto de ação atual de HVAC (opcional)",
|
||||
"preset_dp": "Predefinições DP (opcional)",
|
||||
"preset_set": "Conjunto de predefinições (opcional)",
|
||||
"eco_dp": "Eco DP (opcional)",
|
||||
"eco_value": "Valor ECO (opcional)",
|
||||
"heuristic_action": "Ativar ação heurística (opcional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configurar dispositivo Tuya",
|
||||
"description": "Configuração básica para o ID do dispositivo `{device_id}`.",
|
||||
"data": {
|
||||
"friendly_name": "Nome fantasia",
|
||||
"host": "Host",
|
||||
"local_key": "Local key",
|
||||
"protocol_version": "Versão do protocolo",
|
||||
"scan_interval": "Intervalo de escaneamento (segundos, somente quando não estiver atualizando automaticamente)",
|
||||
"entities": "Entidades (desmarque uma entidade para removê-la)"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"title": "Adicionar nova entidade",
|
||||
"description": "Por favor, preencha os detalhes de uma entidade com o tipo `{platform}`. Todas as configurações, exceto `ID`, podem ser alteradas na página Opções posteriormente.",
|
||||
"data": {
|
||||
"id": "ID",
|
||||
"friendly_name": "Name fantasia",
|
||||
"current": "Atual",
|
||||
"current_consumption": "Consumo atual",
|
||||
"voltage": "Voltagem",
|
||||
"commands_set": "Conjunto de comandos Open_Close_Stop",
|
||||
"positioning_mode": "Modo de posicão",
|
||||
"current_position_dp": "Posição atual (somente para o modo de posição)",
|
||||
"set_position_dp": "Definir posição (somente para o modo de posição)",
|
||||
"position_inverted": "Inverter posição 0-100 (somente para o modo de posição)",
|
||||
"span_time": "Tempo de abertura completo, em segundos. (somente para o modo temporizado)",
|
||||
"unit_of_measurement": "Unidade de medida",
|
||||
"device_class": "Classe do dispositivo",
|
||||
"scaling": "Fator de escala",
|
||||
"state_on": "Valor On",
|
||||
"state_off": "Valor Off",
|
||||
"powergo_dp": "Potência DP (Geralmente 25 ou 2)",
|
||||
"idle_status_value": "Status ocioso (separado por vírgula)",
|
||||
"returning_status_value": "Status de retorno",
|
||||
"docked_status_value": "Status docked (separado por vírgula)",
|
||||
"fault_dp": "Falha DP (Geralmente 11)",
|
||||
"battery_dp": "Status da bateria DP (normalmente 14)",
|
||||
"mode_dp": "Modo DP (Geralmente 27)",
|
||||
"modes": "Lista de modos",
|
||||
"return_mode": "Modo de retorno para base",
|
||||
"fan_speed_dp": "Velocidades do ventilador DP (normalmente 30)",
|
||||
"fan_speeds": "Lista de velocidades do ventilador (separadas por vírgulas)",
|
||||
"clean_time_dp": "Tempo de Limpeza DP (Geralmente 33)",
|
||||
"clean_area_dp": "Área Limpa DP (Geralmente 32)",
|
||||
"clean_record_dp": "Limpar Registro DP (Geralmente 34)",
|
||||
"locate_dp": "Localize DP (Geralmente 31)",
|
||||
"paused_state": "Estado de pausa (pausa, pausado, etc)",
|
||||
"stop_status": "Status de parada",
|
||||
"brightness": "Brilho (somente para cor branca)",
|
||||
"brightness_lower": "Valor mais baixo do brilho",
|
||||
"brightness_upper": "Valor mais alto do brilho",
|
||||
"color_temp": "Temperatura da cor",
|
||||
"color_temp_reverse": "Temperatura da cor reversa",
|
||||
"color": "Cor",
|
||||
"color_mode": "Modo de cor",
|
||||
"color_temp_min_kelvin": "Minima temperatura de cor em K",
|
||||
"color_temp_max_kelvin": "Máxima temperatura de cor em K",
|
||||
"music_mode": "Modo de música disponível",
|
||||
"scene": "Cena",
|
||||
"fan_speed_control": "dps de controle de velocidade do ventilador",
|
||||
"fan_oscillating_control": "dps de controle oscilante do ventilador",
|
||||
"fan_speed_min": "velocidade mínima do ventilador inteiro",
|
||||
"fan_speed_max": "velocidade máxima do ventilador inteiro",
|
||||
"fan_speed_ordered_list": "Lista de modos de velocidade do ventilador (substitui a velocidade min/max)",
|
||||
"fan_direction":"direção do ventilador dps",
|
||||
"fan_direction_forward": "string de dps para frente",
|
||||
"fan_direction_reverse": "string dps reversa",
|
||||
"current_temperature_dp": "Temperatura atual",
|
||||
"target_temperature_dp": "Temperatura alvo",
|
||||
"temperature_step": "Etapa de temperatura (opcional)",
|
||||
"max_temperature_dp": "Max Temperatura (opcional)",
|
||||
"min_temperature_dp": "Min Temperatura (opcional)",
|
||||
"precision": "Precisão (opcional, para valores de DPs)",
|
||||
"target_precision": "Precisão do alvo (opcional, para valores de DPs)",
|
||||
"temperature_unit": "Unidade de Temperatura (opcional)",
|
||||
"hvac_mode_dp": "Modo HVAC DP (opcional)",
|
||||
"hvac_mode_set": "Conjunto de modo HVAC (opcional)",
|
||||
"hvac_action_dp": "Ação atual de HVAC DP (opcional)",
|
||||
"hvac_action_set": "Conjunto de ação atual de HVAC (opcional)",
|
||||
"preset_dp": "Predefinições DP (opcional)",
|
||||
"preset_set": "Conjunto de predefinições (opcional)",
|
||||
"eco_dp": "Eco DP (opcional)",
|
||||
"eco_value": "Valor ECO (opcional)",
|
||||
"heuristic_action": "Ativar ação heurística (opcional)"
|
||||
}
|
||||
},
|
||||
"yaml_import": {
|
||||
"title": "Não suportado",
|
||||
"description": "As opções não podem ser editadas quando configuradas via YAML."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "LocalTuya"
|
||||
}
|
258
custom_components/localtuya/vacuum.py
Normal file
258
custom_components/localtuya/vacuum.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""Platform to locally control Tuya-based vacuum devices."""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.vacuum import (
|
||||
DOMAIN,
|
||||
STATE_CLEANING,
|
||||
STATE_DOCKED,
|
||||
STATE_IDLE,
|
||||
STATE_RETURNING,
|
||||
STATE_PAUSED,
|
||||
STATE_ERROR,
|
||||
SUPPORT_BATTERY,
|
||||
SUPPORT_FAN_SPEED,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_RETURN_HOME,
|
||||
SUPPORT_START,
|
||||
SUPPORT_STATE,
|
||||
SUPPORT_STATUS,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_LOCATE,
|
||||
StateVacuumEntity,
|
||||
)
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
from .const import (
|
||||
CONF_POWERGO_DP,
|
||||
CONF_IDLE_STATUS_VALUE,
|
||||
CONF_RETURNING_STATUS_VALUE,
|
||||
CONF_DOCKED_STATUS_VALUE,
|
||||
CONF_BATTERY_DP,
|
||||
CONF_MODE_DP,
|
||||
CONF_MODES,
|
||||
CONF_FAN_SPEED_DP,
|
||||
CONF_FAN_SPEEDS,
|
||||
CONF_CLEAN_TIME_DP,
|
||||
CONF_CLEAN_AREA_DP,
|
||||
CONF_CLEAN_RECORD_DP,
|
||||
CONF_LOCATE_DP,
|
||||
CONF_FAULT_DP,
|
||||
CONF_PAUSED_STATE,
|
||||
CONF_RETURN_MODE,
|
||||
CONF_STOP_STATUS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLEAN_TIME = "clean_time"
|
||||
CLEAN_AREA = "clean_area"
|
||||
CLEAN_RECORD = "clean_record"
|
||||
MODES_LIST = "cleaning_mode_list"
|
||||
MODE = "cleaning_mode"
|
||||
FAULT = "fault"
|
||||
|
||||
DEFAULT_IDLE_STATUS = "standby,sleep"
|
||||
DEFAULT_RETURNING_STATUS = "docking"
|
||||
DEFAULT_DOCKED_STATUS = "charging,chargecompleted"
|
||||
DEFAULT_MODES = "smart,wall_follow,spiral,single"
|
||||
DEFAULT_FAN_SPEEDS = "low,normal,high"
|
||||
DEFAULT_PAUSED_STATE = "paused"
|
||||
DEFAULT_RETURN_MODE = "chargego"
|
||||
DEFAULT_STOP_STATUS = "standby"
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
"""Return schema used in config flow."""
|
||||
return {
|
||||
vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str,
|
||||
vol.Required(CONF_POWERGO_DP): vol.In(dps),
|
||||
vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str,
|
||||
vol.Optional(
|
||||
CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS
|
||||
): str,
|
||||
vol.Optional(CONF_BATTERY_DP): vol.In(dps),
|
||||
vol.Optional(CONF_MODE_DP): vol.In(dps),
|
||||
vol.Optional(CONF_MODES, default=DEFAULT_MODES): str,
|
||||
vol.Optional(CONF_RETURN_MODE, default=DEFAULT_RETURN_MODE): str,
|
||||
vol.Optional(CONF_FAN_SPEED_DP): vol.In(dps),
|
||||
vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str,
|
||||
vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps),
|
||||
vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps),
|
||||
vol.Optional(CONF_CLEAN_RECORD_DP): vol.In(dps),
|
||||
vol.Optional(CONF_LOCATE_DP): vol.In(dps),
|
||||
vol.Optional(CONF_FAULT_DP): vol.In(dps),
|
||||
vol.Optional(CONF_PAUSED_STATE, default=DEFAULT_PAUSED_STATE): str,
|
||||
vol.Optional(CONF_STOP_STATUS, default=DEFAULT_STOP_STATUS): str,
|
||||
}
|
||||
|
||||
|
||||
class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity):
|
||||
"""Tuya vacuum device."""
|
||||
|
||||
def __init__(self, device, config_entry, switchid, **kwargs):
|
||||
"""Initialize a new LocaltuyaVacuum."""
|
||||
super().__init__(device, config_entry, switchid, _LOGGER, **kwargs)
|
||||
self._state = None
|
||||
self._battery_level = None
|
||||
self._attrs = {}
|
||||
|
||||
self._idle_status_list = []
|
||||
if self.has_config(CONF_IDLE_STATUS_VALUE):
|
||||
self._idle_status_list = self._config[CONF_IDLE_STATUS_VALUE].split(",")
|
||||
|
||||
self._modes_list = []
|
||||
if self.has_config(CONF_MODES):
|
||||
self._modes_list = self._config[CONF_MODES].split(",")
|
||||
self._attrs[MODES_LIST] = self._modes_list
|
||||
|
||||
self._docked_status_list = []
|
||||
if self.has_config(CONF_DOCKED_STATUS_VALUE):
|
||||
self._docked_status_list = self._config[CONF_DOCKED_STATUS_VALUE].split(",")
|
||||
|
||||
self._fan_speed_list = []
|
||||
if self.has_config(CONF_FAN_SPEEDS):
|
||||
self._fan_speed_list = self._config[CONF_FAN_SPEEDS].split(",")
|
||||
|
||||
self._fan_speed = ""
|
||||
self._cleaning_mode = ""
|
||||
|
||||
print("Initialized vacuum [{}]".format(self.name))
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = (
|
||||
SUPPORT_START
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_STOP
|
||||
| SUPPORT_STATUS
|
||||
| SUPPORT_STATE
|
||||
)
|
||||
|
||||
if self.has_config(CONF_RETURN_MODE):
|
||||
supported_features = supported_features | SUPPORT_RETURN_HOME
|
||||
if self.has_config(CONF_FAN_SPEED_DP):
|
||||
supported_features = supported_features | SUPPORT_FAN_SPEED
|
||||
if self.has_config(CONF_BATTERY_DP):
|
||||
supported_features = supported_features | SUPPORT_BATTERY
|
||||
if self.has_config(CONF_LOCATE_DP):
|
||||
supported_features = supported_features | SUPPORT_LOCATE
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the vacuum state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the current battery level."""
|
||||
return self._battery_level
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the specific state attributes of this vacuum cleaner."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def fan_speed(self):
|
||||
"""Return the current fan speed."""
|
||||
return self._fan_speed
|
||||
|
||||
@property
|
||||
def fan_speed_list(self) -> list:
|
||||
"""Return the list of available fan speeds."""
|
||||
return self._fan_speed_list
|
||||
|
||||
async def async_start(self, **kwargs):
|
||||
"""Turn the vacuum on and start cleaning."""
|
||||
await self._device.set_dp(True, self._config[CONF_POWERGO_DP])
|
||||
|
||||
async def async_pause(self, **kwargs):
|
||||
"""Stop the vacuum cleaner, do not return to base."""
|
||||
await self._device.set_dp(False, self._config[CONF_POWERGO_DP])
|
||||
|
||||
async def async_return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock."""
|
||||
if self.has_config(CONF_RETURN_MODE):
|
||||
await self._device.set_dp(
|
||||
self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP]
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Missing command for return home in commands set.")
|
||||
|
||||
async def async_stop(self, **kwargs):
|
||||
"""Turn the vacuum off stopping the cleaning."""
|
||||
if self.has_config(CONF_STOP_STATUS):
|
||||
await self._device.set_dp(
|
||||
self._config[CONF_STOP_STATUS], self._config[CONF_MODE_DP]
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Missing command for stop in commands set.")
|
||||
|
||||
async def async_clean_spot(self, **kwargs):
|
||||
"""Perform a spot clean-up."""
|
||||
return None
|
||||
|
||||
async def async_locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner."""
|
||||
if self.has_config(CONF_LOCATE_DP):
|
||||
await self._device.set_dp("", self._config[CONF_LOCATE_DP])
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed, **kwargs):
|
||||
"""Set the fan speed."""
|
||||
await self._device.set_dp(fan_speed, self._config[CONF_FAN_SPEED_DP])
|
||||
|
||||
async def async_send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
if command == "set_mode" and "mode" in params:
|
||||
mode = params["mode"]
|
||||
await self._device.set_dp(mode, self._config[CONF_MODE_DP])
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
state_value = str(self.dps(self._dp_id))
|
||||
|
||||
if state_value in self._idle_status_list:
|
||||
self._state = STATE_IDLE
|
||||
elif state_value in self._docked_status_list:
|
||||
self._state = STATE_DOCKED
|
||||
elif state_value == self._config[CONF_RETURNING_STATUS_VALUE]:
|
||||
self._state = STATE_RETURNING
|
||||
elif state_value == self._config[CONF_PAUSED_STATE]:
|
||||
self._state = STATE_PAUSED
|
||||
else:
|
||||
self._state = STATE_CLEANING
|
||||
|
||||
if self.has_config(CONF_BATTERY_DP):
|
||||
self._battery_level = self.dps_conf(CONF_BATTERY_DP)
|
||||
|
||||
self._cleaning_mode = ""
|
||||
if self.has_config(CONF_MODES):
|
||||
self._cleaning_mode = self.dps_conf(CONF_MODE_DP)
|
||||
self._attrs[MODE] = self._cleaning_mode
|
||||
|
||||
self._fan_speed = ""
|
||||
if self.has_config(CONF_FAN_SPEEDS):
|
||||
self._fan_speed = self.dps_conf(CONF_FAN_SPEED_DP)
|
||||
|
||||
if self.has_config(CONF_CLEAN_TIME_DP):
|
||||
self._attrs[CLEAN_TIME] = self.dps_conf(CONF_CLEAN_TIME_DP)
|
||||
|
||||
if self.has_config(CONF_CLEAN_AREA_DP):
|
||||
self._attrs[CLEAN_AREA] = self.dps_conf(CONF_CLEAN_AREA_DP)
|
||||
|
||||
if self.has_config(CONF_CLEAN_RECORD_DP):
|
||||
self._attrs[CLEAN_RECORD] = self.dps_conf(CONF_CLEAN_RECORD_DP)
|
||||
|
||||
if self.has_config(CONF_FAULT_DP):
|
||||
self._attrs[FAULT] = self.dps_conf(CONF_FAULT_DP)
|
||||
if self._attrs[FAULT] != 0:
|
||||
self._state = STATE_ERROR
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema)
|
Reference in New Issue
Block a user