Merge remote-tracking branch 'upstream/master' into update_interval

This commit is contained in:
Martín Villagra
2021-12-16 18:44:41 -03:00
10 changed files with 247 additions and 8 deletions

View File

@@ -107,6 +107,8 @@ Start by going to Configuration - Integration and pressing the "+" button to cre
Wait for 6 seconds for the scanning of the devices in your LAN. Then, a drop-down menu will appear containing the list of detected devices: you can Wait for 6 seconds for the scanning of the devices in your LAN. Then, a drop-down menu will appear containing the list of detected devices: you can
select one of these, or manually input all the parameters. select one of these, or manually input all the parameters.
> **Note: The tuya app on your device must be closed for the following steps to work reliably.**
![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png)
If you have selected one entry, you only need to input the device's Friendly Name and the localKey. If you have selected one entry, you only need to input the device's Friendly Name and the localKey.

View File

@@ -119,6 +119,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self.dps_to_request = {} self.dps_to_request = {}
self._is_closing = False self._is_closing = False
self._connect_task = None self._connect_task = None
self._disconnect_task = None
self._unsub_interval = None self._unsub_interval = None
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
@@ -137,6 +138,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._connect_task = asyncio.create_task(self._make_connection()) self._connect_task = asyncio.create_task(self._make_connection())
async def _make_connection(self): async def _make_connection(self):
"""Subscribe localtuya entity events."""
self.debug("Connecting to %s", self._config_entry[CONF_HOST]) self.debug("Connecting to %s", self._config_entry[CONF_HOST])
try: try:
@@ -155,6 +157,20 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
raise Exception("Failed to retrieve status") raise Exception("Failed to retrieve status")
self.status_updated(status) self.status_updated(status)
def _new_entity_handler(entity_id):
self.debug(
"New entity %s was added to %s",
entity_id,
self._config_entry[CONF_HOST],
)
self._dispatch_status()
signal = f"localtuya_entity_{self._config_entry[CONF_DEVICE_ID]}"
self._disconnect_task = async_dispatcher_connect(
self._hass, signal, _new_entity_handler
)
if ( if (
CONF_SCAN_INTERVAL in self._config_entry CONF_SCAN_INTERVAL in self._config_entry
and self._config_entry[CONF_SCAN_INTERVAL] > 0 and self._config_entry[CONF_SCAN_INTERVAL] > 0
@@ -183,6 +199,8 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
await self._connect_task await self._connect_task
if self._interface is not None: if self._interface is not None:
await self._interface.close() await self._interface.close()
if self._disconnect_task is not None:
self._disconnect_task()
async def set_dp(self, state, dp_index): async def set_dp(self, state, dp_index):
"""Change value of a DP of the Tuya device.""" """Change value of a DP of the Tuya device."""
@@ -212,7 +230,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
def status_updated(self, status): def status_updated(self, status):
"""Device updated status.""" """Device updated status."""
self._status.update(status) self._status.update(status)
self._dispatch_status()
def _dispatch_status(self):
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}" signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self._hass, signal, self._status) async_dispatcher_send(self._hass, signal, self._status)
@@ -262,10 +282,14 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
self.schedule_update_ha_state() self.schedule_update_ha_state()
signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}"
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect(self.hass, signal, _update_handler) async_dispatcher_connect(self.hass, signal, _update_handler)
) )
signal = f"localtuya_entity_{self._config_entry.data[CONF_DEVICE_ID]}"
async_dispatcher_send(self.hass, signal, self.entity_id)
@property @property
def device_info(self): def device_info(self):
"""Return device information for the device registry.""" """Return device information for the device registry."""

View File

@@ -46,6 +46,15 @@ DATA_DISCOVERY = "discovery"
DOMAIN = "localtuya" DOMAIN = "localtuya"
# Platforms in this list must support config flows # Platforms in this list must support config flows
PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] PLATFORMS = [
"binary_sensor",
"cover",
"fan",
"light",
"number",
"select",
"sensor",
"switch",
]
TUYA_DEVICE = "tuya_device" TUYA_DEVICE = "tuya_device"

View File

@@ -79,7 +79,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity):
"""Get the list of available speeds.""" """Get the list of available speeds."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
async def async_turn_on(self, speed: str = None, **kwargs) -> None: async def async_turn_on(
self,
speed: str = None,
percentage: int = None,
preset_mode: str = None,
**kwargs,
) -> None:
"""Turn on the entity.""" """Turn on the entity."""
await self._device.set_dp(True, self._dp_id) await self._device.set_dp(True, self._dp_id)
if speed is not None: if speed is not None:

View File

@@ -0,0 +1,87 @@
"""Platform to present any Tuya DP as a number."""
import logging
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 .common import LocalTuyaEntity, async_setup_entry
_LOGGER = logging.getLogger(__name__)
CONF_MIN_VALUE = "min_value"
CONF_MAX_VALUE = "max_value"
DEFAULT_MIN = 0
DEFAULT_MAX = 100000
def flow_schema(dps):
"""Return schema used in config flow."""
return {
vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All(
vol.Coerce(float),
vol.Range(min=-1000000.0, max=1000000.0),
),
vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All(
vol.Coerce(float),
vol.Range(min=-1000000.0, max=1000000.0),
),
}
class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
"""Representation of a Tuya Number."""
def __init__(
self,
device,
config_entry,
sensorid,
**kwargs,
):
"""Initialize the Tuya sensor."""
super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs)
self._state = STATE_UNKNOWN
self._min_value = DEFAULT_MIN
if CONF_MIN_VALUE in self._config:
self._min_value = self._config.get(CONF_MIN_VALUE)
self._max_value = self._config.get(CONF_MAX_VALUE)
@property
def value(self) -> float:
"""Return sensor state."""
return self._state
@property
def min_value(self) -> float:
"""Return the minimum value."""
return self._min_value
@property
def max_value(self) -> float:
"""Return the maximum value."""
return self._max_value
@property
def device_class(self):
"""Return the class of this device."""
return self._config.get(CONF_DEVICE_CLASS)
async def async_set_value(self, value: float) -> None:
"""Update the current value."""
await self._device.set_dp(value, self._dp_id)
def status_updated(self):
"""Device status was updated."""
state = self.dps(self._dp_id)
self._state = state
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema)

View File

@@ -0,0 +1,103 @@
"""Platform to present any Tuya DP as an enumeration."""
import logging
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 .common import LocalTuyaEntity, async_setup_entry
_LOGGER = logging.getLogger(__name__)
CONF_OPTIONS = "select_options"
CONF_OPTIONS_FRIENDLY = "select_options_friendly"
def flow_schema(dps):
"""Return schema used in config flow."""
return {
vol.Required(CONF_OPTIONS): str,
vol.Optional(CONF_OPTIONS_FRIENDLY): str,
}
class LocaltuyaSelect(LocalTuyaEntity, SelectEntity):
"""Representation of a Tuya Enumeration."""
def __init__(
self,
device,
config_entry,
sensorid,
**kwargs,
):
"""Initialize the Tuya sensor."""
super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs)
self._state = STATE_UNKNOWN
self._state_friendly = ""
self._valid_options = self._config.get(CONF_OPTIONS).split(";")
# Set Display options
self._display_options = []
display_options_str = ""
if CONF_OPTIONS_FRIENDLY in self._config:
display_options_str = self._config.get(CONF_OPTIONS_FRIENDLY).strip()
_LOGGER.debug("Display Options Configured: %s", display_options_str)
if display_options_str.find(";") >= 0:
self._display_options = display_options_str.split(";")
elif len(display_options_str.strip()) > 0:
self._display_options.append(display_options_str)
else:
# Default display string to raw string
_LOGGER.debug("No Display options configured - defaulting to raw values")
self._display_options = self._valid_options
_LOGGER.debug(
"Total Raw Options: %s - Total Display Options: %s",
str(len(self._valid_options)),
str(len(self._display_options)),
)
if len(self._valid_options) > len(self._display_options):
# If list of display items smaller than list of valid items,
# then default remaining items to be the raw value
_LOGGER.debug(
"Valid options is larger than display options - \
filling up with raw values"
)
for i in range(len(self._display_options), len(self._valid_options)):
self._display_options.append(self._valid_options[i])
@property
def current_option(self) -> str:
"""Return the current value."""
return self._state_friendly
@property
def options(self) -> list:
"""Return the list of values."""
return self._display_options
@property
def device_class(self):
"""Return the class of this device."""
return self._config.get(CONF_DEVICE_CLASS)
async def async_select_option(self, option: str) -> None:
"""Update the current value."""
option_value = self._valid_options[self._display_options.index(option)]
_LOGGER.debug("Sending Option: " + option + " -> " + option_value)
await self._device.set_dp(option_value, self._dp_id)
def status_updated(self):
"""Device status was updated."""
state = self.dps(self._dp_id)
self._state_friendly = self._display_options[self._valid_options.index(state)]
self._state = state
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema)

View File

@@ -75,7 +75,11 @@
"fan_oscillating_control": "Fan Oscillating Control", "fan_oscillating_control": "Fan Oscillating Control",
"fan_speed_low": "Fan Low Speed Setting", "fan_speed_low": "Fan Low Speed Setting",
"fan_speed_medium": "Fan Medium Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting",
"fan_speed_high": "Fan High 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 ;"
} }
} }
} }
@@ -128,7 +132,11 @@
"fan_oscillating_control": "Fan Oscillating Control", "fan_oscillating_control": "Fan Oscillating Control",
"fan_speed_low": "Fan Low Speed Setting", "fan_speed_low": "Fan Low Speed Setting",
"fan_speed_medium": "Fan Medium Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting",
"fan_speed_high": "Fan High 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 ;"
} }
}, },
"yaml_import": { "yaml_import": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "Local Tuya", "name": "Local Tuya",
"domains": ["climate", "cover", "fan", "light", "sensor", "switch"], "domains": ["climate", "cover", "fan", "light", "number", "select", "sensor", "switch"],
"homeassistant": "0.116.0", "homeassistant": "0.116.0",
"iot_class": ["Local Push"] "iot_class": ["Local Push"]
} }

View File

@@ -3,7 +3,7 @@ codespell==2.0.0
flake8==3.9.2 flake8==3.9.2
mypy==0.901 mypy==0.901
pydocstyle==6.1.1 pydocstyle==6.1.1
cryptography==3.2 cryptography==3.3.2
pylint==2.8.2 pylint==2.8.2
pylint-strict-informational==0.1 pylint-strict-informational==0.1
homeassistant==2021.1.4 homeassistant==2021.7.1

View File

@@ -12,7 +12,7 @@ warn_incomplete_stub = true
warn_redundant_casts = true warn_redundant_casts = true
warn_unused_configs = true warn_unused_configs = true
[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] [mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,homeassistant.components.select.*,homeassistant.components.number.*]
strict = true strict = true
ignore_errors = false ignore_errors = false
warn_unreachable = true warn_unreachable = true