Merge branch 'master' into master
This commit is contained in:
@@ -92,9 +92,13 @@ If you have selected one entry, you only need to input the device's Friendly Nam
|
||||
|
||||
Setting the scan interval is optional, it is only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues.
|
||||
|
||||
Setting the 'Manual DPS To Add' is optional, it is only needed if the device doesn't advertise the DPS correctly until the entity has been properly initiailised. This setting can often be avoided by first connecting/initialising the device with the Tuya App, then closing the app and then adding the device in the integration.
|
||||
|
||||
Setting the 'DPIDs to send in RESET command' is optional. It is used when a device doesn't respond to any Tuya commands after a power cycle, but can be connected to (zombie state). The DPids will vary between devices, but typically "18,19,20" is used (and will be the default if none specified). If the wrong entries are added here, then the device may not come out of the zombie state. Typically only sensor DPIDs entered here.
|
||||
|
||||
Once you press "Submit", the connection is tested to check that everything works.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up.
|
||||
|
@@ -53,6 +53,8 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity):
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
super().status_updated()
|
||||
|
||||
state = str(self.dps(self._dp_id)).lower()
|
||||
if state == self._config[CONF_STATE_ON].lower():
|
||||
self._is_on = True
|
||||
@@ -63,6 +65,11 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity):
|
||||
"State for entity %s did not match state patterns", self.entity_id
|
||||
)
|
||||
|
||||
# No need to restore state for a sensor
|
||||
async def restore_state_when_connected(self):
|
||||
"""Do nothing for a sensor."""
|
||||
return
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
async_setup_entry, DOMAIN, LocaltuyaBinarySensor, flow_schema
|
||||
|
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@@ -31,6 +32,10 @@ from .const import (
|
||||
DATA_CLOUD,
|
||||
DOMAIN,
|
||||
TUYA_DEVICES,
|
||||
CONF_DEFAULT_VALUE,
|
||||
ATTR_STATE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
CONF_RESET_DPIDS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -91,6 +96,8 @@ async def async_setup_entry(
|
||||
entity_config[CONF_ID],
|
||||
)
|
||||
)
|
||||
# Once the entities have been created, add to the TuyaDevice instance
|
||||
tuyainterface.add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -135,13 +142,31 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._connect_task = None
|
||||
self._disconnect_task = None
|
||||
self._unsub_interval = None
|
||||
self._entities = []
|
||||
self._local_key = self._dev_config_entry[CONF_LOCAL_KEY]
|
||||
self._default_reset_dpids = None
|
||||
if CONF_RESET_DPIDS in self._dev_config_entry:
|
||||
reset_ids_str = self._dev_config_entry[CONF_RESET_DPIDS].split(",")
|
||||
|
||||
self._default_reset_dpids = []
|
||||
for reset_id in reset_ids_str:
|
||||
self._default_reset_dpids.append(int(reset_id.strip()))
|
||||
|
||||
self.set_logger(_LOGGER, self._dev_config_entry[CONF_DEVICE_ID])
|
||||
|
||||
# This has to be done in case the device type is type_0d
|
||||
for entity in self._dev_config_entry[CONF_ENTITIES]:
|
||||
self.dps_to_request[entity[CONF_ID]] = None
|
||||
|
||||
def add_entities(self, entities):
|
||||
"""Set the entities associated with this device."""
|
||||
self._entities.extend(entities)
|
||||
|
||||
@property
|
||||
def is_connecting(self):
|
||||
"""Return whether device is currently connecting."""
|
||||
return self._connect_task is not None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return if connected to device."""
|
||||
@@ -165,14 +190,65 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self,
|
||||
)
|
||||
self._interface.add_dps_to_request(self.dps_to_request)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed")
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
if self._interface is not None:
|
||||
try:
|
||||
self.debug("Retrieving initial state")
|
||||
status = await self._interface.status()
|
||||
if status is None:
|
||||
raise Exception("Failed to retrieve status")
|
||||
|
||||
self._interface.start_heartbeat()
|
||||
self.status_updated(status)
|
||||
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
try:
|
||||
self.debug(
|
||||
"Initial state update failed, trying reset command "
|
||||
+ "for DP IDs: %s",
|
||||
self._default_reset_dpids,
|
||||
)
|
||||
await self._interface.reset(self._default_reset_dpids)
|
||||
|
||||
self.debug("Update completed, retrying initial state")
|
||||
status = await self._interface.status()
|
||||
if status is None or not status:
|
||||
raise Exception("Failed to retrieve status") from ex
|
||||
|
||||
self._interface.start_heartbeat()
|
||||
self.status_updated(status)
|
||||
|
||||
except UnicodeDecodeError as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s",
|
||||
type(e),
|
||||
)
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed"
|
||||
)
|
||||
if "json.decode" in str(type(e)):
|
||||
await self.update_local_key()
|
||||
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
if self._interface is not None:
|
||||
# Attempt to restore status for all entities that need to first set
|
||||
# the DPS value before the device will respond with status.
|
||||
for entity in self._entities:
|
||||
await entity.restore_state_when_connected()
|
||||
|
||||
def _new_entity_handler(entity_id):
|
||||
self.debug(
|
||||
"New entity %s was added to %s",
|
||||
@@ -195,22 +271,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
self._async_refresh,
|
||||
timedelta(seconds=self._dev_config_entry[CONF_SCAN_INTERVAL]),
|
||||
)
|
||||
except UnicodeDecodeError as e: # pylint: disable=broad-except
|
||||
self.exception(
|
||||
f"Connect to {self._dev_config_entry[CONF_HOST]} failed: %s", type(e)
|
||||
)
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed")
|
||||
if "json.decode" in str(type(e)):
|
||||
await self.update_local_key()
|
||||
|
||||
if self._interface is not None:
|
||||
await self._interface.close()
|
||||
self._interface = None
|
||||
self._connect_task = None
|
||||
|
||||
async def update_local_key(self):
|
||||
@@ -254,7 +315,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
|
||||
try:
|
||||
await self._interface.set_dp(state, dp_index)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.exception("Failed to set DP %d to %d", dp_index, state)
|
||||
self.exception("Failed to set DP %d to %s", dp_index, str(state))
|
||||
else:
|
||||
self.error(
|
||||
"Not connected to device %s", self._dev_config_entry[CONF_FRIENDLY_NAME]
|
||||
@@ -305,6 +366,17 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
self._config = get_entity_config(config_entry, dp_id)
|
||||
self._dp_id = dp_id
|
||||
self._status = {}
|
||||
self._state = None
|
||||
self._last_state = None
|
||||
|
||||
# Default value is available to be provided by Platform entities if required
|
||||
self._default_value = self._config.get(CONF_DEFAULT_VALUE)
|
||||
|
||||
""" Restore on connect setting is available to be provided by Platform entities
|
||||
if required"""
|
||||
self._restore_on_reconnect = (
|
||||
self._config.get(CONF_RESTORE_ON_RECONNECT) or False
|
||||
)
|
||||
self.set_logger(logger, self._dev_config_entry[CONF_DEVICE_ID])
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
@@ -325,6 +397,8 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
self._status = status.copy()
|
||||
if status:
|
||||
self.status_updated()
|
||||
|
||||
# Update HA
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
signal = f"localtuya_{self._dev_config_entry[CONF_DEVICE_ID]}"
|
||||
@@ -336,6 +410,22 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
signal = f"localtuya_entity_{self._dev_config_entry[CONF_DEVICE_ID]}"
|
||||
async_dispatcher_send(self.hass, signal, self.entity_id)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return entity specific state attributes to be saved.
|
||||
|
||||
These attributes are then available for restore when the
|
||||
entity is restored at startup.
|
||||
"""
|
||||
attributes = {}
|
||||
if self._state is not None:
|
||||
attributes[ATTR_STATE] = self._state
|
||||
elif self._last_state is not None:
|
||||
attributes[ATTR_STATE] = self._last_state
|
||||
|
||||
self.debug("Entity %s - Additional attributes: %s", self.name, attributes)
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device information for the device registry."""
|
||||
@@ -408,9 +498,89 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
|
||||
|
||||
Override in subclasses and update entity specific state.
|
||||
"""
|
||||
state = self.dps(self._dp_id)
|
||||
self._state = state
|
||||
|
||||
# Keep record in last_state as long as not during connection/re-connection,
|
||||
# as last state will be used to restore the previous state
|
||||
if (state is not None) and (not self._device.is_connecting):
|
||||
self._last_state = state
|
||||
|
||||
def status_restored(self, stored_state):
|
||||
"""Device status was restored.
|
||||
|
||||
Override in subclasses and update entity specific state.
|
||||
"""
|
||||
raw_state = stored_state.attributes.get(ATTR_STATE)
|
||||
if raw_state is not None:
|
||||
self._last_state = raw_state
|
||||
self.debug(
|
||||
"Restoring state for entity: %s - state: %s",
|
||||
self.name,
|
||||
str(self._last_state),
|
||||
)
|
||||
|
||||
def default_value(self):
|
||||
"""Return default value of this entity.
|
||||
|
||||
Override in subclasses to specify the default value for the entity.
|
||||
"""
|
||||
# Check if default value has been set - if not, default to the entity defaults.
|
||||
if self._default_value is None:
|
||||
self._default_value = self.entity_default_value()
|
||||
|
||||
return self._default_value
|
||||
|
||||
def entity_default_value(self): # pylint: disable=no-self-use
|
||||
"""Return default value of the entity type.
|
||||
|
||||
Override in subclasses to specify the default value for the entity.
|
||||
"""
|
||||
return 0
|
||||
|
||||
@property
|
||||
def restore_on_reconnect(self):
|
||||
"""Return whether the last state should be restored on a reconnect.
|
||||
|
||||
Useful where the device loses settings if powered off
|
||||
"""
|
||||
return self._restore_on_reconnect
|
||||
|
||||
async def restore_state_when_connected(self):
|
||||
"""Restore if restore_on_reconnect is set, or if no status has been yet found.
|
||||
|
||||
Which indicates a DPS that needs to be set before it starts returning
|
||||
status.
|
||||
"""
|
||||
if not self.restore_on_reconnect and (str(self._dp_id) in self._status):
|
||||
self.debug(
|
||||
"Entity %s (DP %d) - Not restoring as restore on reconnect is \
|
||||
disabled for this entity and the entity has an initial status",
|
||||
self.name,
|
||||
self._dp_id,
|
||||
)
|
||||
return
|
||||
|
||||
self.debug("Attempting to restore state for entity: %s", self.name)
|
||||
# Attempt to restore the current state - in case reset.
|
||||
restore_state = self._state
|
||||
|
||||
# If no state stored in the entity currently, go from last saved state
|
||||
if (restore_state == STATE_UNKNOWN) | (restore_state is None):
|
||||
self.debug("No current state for entity")
|
||||
restore_state = self._last_state
|
||||
|
||||
# If no current or saved state, then use the default value
|
||||
if restore_state is None:
|
||||
self.debug("No last restored state - using default")
|
||||
restore_state = self.default_value()
|
||||
|
||||
self.debug(
|
||||
"Entity %s (DP %d) - Restoring state: %s",
|
||||
self.name,
|
||||
self._dp_id,
|
||||
str(restore_state),
|
||||
)
|
||||
|
||||
# Manually initialise
|
||||
await self._device.set_dp(restore_state, self._dp_id)
|
||||
|
@@ -38,12 +38,14 @@ from .const import (
|
||||
CONF_NO_CLOUD,
|
||||
CONF_PRODUCT_NAME,
|
||||
CONF_PROTOCOL_VERSION,
|
||||
CONF_RESET_DPIDS,
|
||||
CONF_SETUP_CLOUD,
|
||||
CONF_USER_ID,
|
||||
DATA_CLOUD,
|
||||
DATA_DISCOVERY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_MANUAL_DPS,
|
||||
)
|
||||
from .discovery import discover
|
||||
|
||||
@@ -88,6 +90,8 @@ CONFIGURE_DEVICE_SCHEMA = vol.Schema(
|
||||
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,
|
||||
vol.Optional(CONF_MANUAL_DPS): str,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -99,6 +103,8 @@ DEVICE_SCHEMA = vol.Schema(
|
||||
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,
|
||||
vol.Optional(CONF_MANUAL_DPS): cv.string,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,6 +146,8 @@ def options_schema(entities):
|
||||
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.Optional(CONF_MANUAL_DPS): str,
|
||||
vol.Optional(CONF_RESET_DPIDS): str,
|
||||
vol.Required(
|
||||
CONF_ENTITIES, description={"suggested_value": entity_names}
|
||||
): cv.multi_select(entity_names),
|
||||
@@ -231,6 +239,8 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
detected_dps = {}
|
||||
|
||||
interface = None
|
||||
|
||||
reset_ids = None
|
||||
try:
|
||||
interface = await pytuya.connect(
|
||||
data[CONF_HOST],
|
||||
@@ -239,7 +249,42 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
float(data[CONF_PROTOCOL_VERSION]),
|
||||
)
|
||||
|
||||
try:
|
||||
detected_dps = await interface.detect_available_dps()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
try:
|
||||
_LOGGER.debug("Initial state update failed, trying reset command")
|
||||
if CONF_RESET_DPIDS in data:
|
||||
reset_ids_str = data[CONF_RESET_DPIDS].split(",")
|
||||
reset_ids = []
|
||||
for reset_id in reset_ids_str:
|
||||
reset_ids.append(int(reset_id.strip()))
|
||||
_LOGGER.debug(
|
||||
"Reset DPIDs configured: %s (%s)",
|
||||
data[CONF_RESET_DPIDS],
|
||||
reset_ids,
|
||||
)
|
||||
await interface.reset(reset_ids)
|
||||
detected_dps = await interface.detect_available_dps()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.debug("No DPS able to be detected")
|
||||
detected_dps = {}
|
||||
|
||||
# if manual DPs are set, merge these.
|
||||
_LOGGER.debug("Detected DPS: %s", detected_dps)
|
||||
if CONF_MANUAL_DPS in data:
|
||||
|
||||
manual_dps_list = [dps.strip() for dps in data[CONF_MANUAL_DPS].split(",")]
|
||||
_LOGGER.debug(
|
||||
"Manual DPS Setting: %s (%s)", data[CONF_MANUAL_DPS], manual_dps_list
|
||||
)
|
||||
# merge the lists
|
||||
for new_dps in manual_dps_list + (reset_ids or []):
|
||||
# If the DPS not in the detected dps list, then add with a
|
||||
# default value indicating that it has been manually added
|
||||
if new_dps not in detected_dps:
|
||||
detected_dps[new_dps] = -1
|
||||
|
||||
except (ConnectionRefusedError, ConnectionResetError) as ex:
|
||||
raise CannotConnect from ex
|
||||
except ValueError as ex:
|
||||
@@ -253,6 +298,8 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
if not detected_dps:
|
||||
raise EmptyDpsList
|
||||
|
||||
_LOGGER.debug("Total DPS: %s", detected_dps)
|
||||
|
||||
return dps_string_list(detected_dps)
|
||||
|
||||
|
||||
|
@@ -35,11 +35,15 @@ CONF_PRODUCT_KEY = "product_key"
|
||||
CONF_PRODUCT_NAME = "product_name"
|
||||
CONF_USER_ID = "user_id"
|
||||
|
||||
|
||||
CONF_ACTION = "action"
|
||||
CONF_ADD_DEVICE = "add_device"
|
||||
CONF_EDIT_DEVICE = "edit_device"
|
||||
CONF_SETUP_CLOUD = "setup_cloud"
|
||||
CONF_NO_CLOUD = "no_cloud"
|
||||
CONF_MANUAL_DPS = "manual_dps_strings"
|
||||
CONF_DEFAULT_VALUE = "dps_default_value"
|
||||
CONF_RESET_DPIDS = "reset_dpids"
|
||||
|
||||
# light
|
||||
CONF_BRIGHTNESS_LOWER = "brightness_lower"
|
||||
@@ -114,3 +118,16 @@ CONF_FAULT_DP = "fault_dp"
|
||||
CONF_PAUSED_STATE = "paused_state"
|
||||
CONF_RETURN_MODE = "return_mode"
|
||||
CONF_STOP_STATUS = "stop_status"
|
||||
|
||||
# number
|
||||
CONF_MIN_VALUE = "min_value"
|
||||
CONF_MAX_VALUE = "max_value"
|
||||
CONF_STEPSIZE_VALUE = "step_size"
|
||||
|
||||
# select
|
||||
CONF_OPTIONS = "select_options"
|
||||
CONF_OPTIONS_FRIENDLY = "select_options_friendly"
|
||||
|
||||
# States
|
||||
ATTR_STATE = "raw_state"
|
||||
CONF_RESTORE_ON_RECONNECT = "restore_on_reconnect"
|
||||
|
@@ -228,5 +228,10 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity):
|
||||
# store the time of the last movement change
|
||||
self._timer_start = time.time()
|
||||
|
||||
# Keep record in last_state as long as not during connection/re-connection,
|
||||
# as last state will be used to restore the previous state
|
||||
if (self._state is not None) and (not self._device.is_connecting):
|
||||
self._last_state = self._state
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaCover, flow_schema)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "localtuya",
|
||||
"name": "LocalTuya integration",
|
||||
"version": "4.0.2",
|
||||
"version": "4.1.0",
|
||||
"documentation": "https://github.com/rospogrigio/localtuya/",
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@@ -8,13 +8,19 @@ from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN
|
||||
|
||||
from .common import LocalTuyaEntity, async_setup_entry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import (
|
||||
CONF_MIN_VALUE,
|
||||
CONF_MAX_VALUE,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
CONF_STEPSIZE_VALUE,
|
||||
)
|
||||
|
||||
CONF_MIN_VALUE = "native_min_value"
|
||||
CONF_MAX_VALUE = "native_max_value"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_MIN = 0
|
||||
DEFAULT_MAX = 100000
|
||||
DEFAULT_STEP = 1.0
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
@@ -28,6 +34,12 @@ def flow_schema(dps):
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=-1000000.0, max=1000000.0),
|
||||
),
|
||||
vol.Required(CONF_STEPSIZE_VALUE, default=DEFAULT_STEP): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=0.0, max=1000000.0),
|
||||
),
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +61,19 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
if CONF_MIN_VALUE in self._config:
|
||||
self._min_value = self._config.get(CONF_MIN_VALUE)
|
||||
|
||||
self._max_value = DEFAULT_MAX
|
||||
if CONF_MAX_VALUE in self._config:
|
||||
self._max_value = self._config.get(CONF_MAX_VALUE)
|
||||
|
||||
self._step_size = DEFAULT_STEP
|
||||
if CONF_STEPSIZE_VALUE in self._config:
|
||||
self._step_size = self._config.get(CONF_STEPSIZE_VALUE)
|
||||
|
||||
# Override standard default value handling to cast to a float
|
||||
default_value = self._config.get(CONF_DEFAULT_VALUE)
|
||||
if default_value is not None:
|
||||
self._default_value = float(default_value)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return sensor state."""
|
||||
@@ -66,6 +89,11 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
"""Return the maximum value."""
|
||||
return self._max_value
|
||||
|
||||
@property
|
||||
def native_step(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
return self._step_size
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
@@ -75,10 +103,10 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity):
|
||||
"""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
|
||||
# Default value is the minimum value
|
||||
def entity_default_value(self):
|
||||
"""Return the minimum value as the default for this entity type."""
|
||||
return self._min_value
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema)
|
||||
|
@@ -62,6 +62,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc")
|
||||
SET = "set"
|
||||
STATUS = "status"
|
||||
HEARTBEAT = "heartbeat"
|
||||
RESET = "reset"
|
||||
UPDATEDPS = "updatedps" # Request refresh of DPS
|
||||
|
||||
PROTOCOL_VERSION_BYTES_31 = b"3.1"
|
||||
@@ -96,6 +97,16 @@ PAYLOAD_DICT = {
|
||||
SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
HEARTBEAT: {"hexByte": 0x09, "command": {}},
|
||||
UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}},
|
||||
RESET: {
|
||||
"hexByte": 0x12,
|
||||
"command": {
|
||||
"gwId": "",
|
||||
"devId": "",
|
||||
"uid": "",
|
||||
"t": "",
|
||||
"dpId": [18, 19, 20],
|
||||
},
|
||||
},
|
||||
},
|
||||
"type_0d": {
|
||||
STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}},
|
||||
@@ -217,6 +228,7 @@ class MessageDispatcher(ContextualLogger):
|
||||
# Heartbeats always respond with sequence number 0, so they can't be waited for like
|
||||
# other messages. This is a hack to allow waiting for heartbeats.
|
||||
HEARTBEAT_SEQNO = -100
|
||||
RESET_SEQNO = -101
|
||||
|
||||
def __init__(self, dev_id, listener):
|
||||
"""Initialize a new MessageBuffer."""
|
||||
@@ -301,7 +313,17 @@ class MessageDispatcher(ContextualLogger):
|
||||
sem.release()
|
||||
elif msg.cmd == 0x12:
|
||||
self.debug("Got normal updatedps response")
|
||||
if self.RESET_SEQNO in self.listeners:
|
||||
sem = self.listeners[self.RESET_SEQNO]
|
||||
self.listeners[self.RESET_SEQNO] = msg
|
||||
sem.release()
|
||||
elif msg.cmd == 0x08:
|
||||
if self.RESET_SEQNO in self.listeners:
|
||||
self.debug("Got reset status update")
|
||||
sem = self.listeners[self.RESET_SEQNO]
|
||||
self.listeners[self.RESET_SEQNO] = msg
|
||||
sem.release()
|
||||
else:
|
||||
self.debug("Got status update")
|
||||
self.listener(msg)
|
||||
else:
|
||||
@@ -381,6 +403,11 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Did connect to the device."""
|
||||
self.transport = transport
|
||||
self.on_connected.set_result(True)
|
||||
|
||||
def start_heartbeat(self):
|
||||
"""Start the heartbeat transmissions with the device."""
|
||||
|
||||
async def heartbeat_loop():
|
||||
"""Continuously send heart beat updates."""
|
||||
@@ -403,8 +430,6 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
self.transport = None
|
||||
transport.close()
|
||||
|
||||
self.transport = transport
|
||||
self.on_connected.set_result(True)
|
||||
self.heartbeater = self.loop.create_task(heartbeat_loop())
|
||||
|
||||
def data_received(self, data):
|
||||
@@ -449,12 +474,13 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
payload = self._generate_payload(command, dps)
|
||||
dev_type = self.dev_type
|
||||
|
||||
# Wait for special sequence number if heartbeat
|
||||
seqno = (
|
||||
MessageDispatcher.HEARTBEAT_SEQNO
|
||||
if command == HEARTBEAT
|
||||
else (self.seqno - 1)
|
||||
)
|
||||
# Wait for special sequence number if heartbeat or reset
|
||||
seqno = self.seqno - 1
|
||||
|
||||
if command == HEARTBEAT:
|
||||
seqno = MessageDispatcher.HEARTBEAT_SEQNO
|
||||
elif command == RESET:
|
||||
seqno = MessageDispatcher.RESET_SEQNO
|
||||
|
||||
self.transport.write(payload)
|
||||
msg = await self.dispatcher.wait_for(seqno)
|
||||
@@ -487,6 +513,15 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
|
||||
"""Send a heartbeat message."""
|
||||
return await self.exchange(HEARTBEAT)
|
||||
|
||||
async def reset(self, dpIds=None):
|
||||
"""Send a reset message (3.3 only)."""
|
||||
if self.version == 3.3:
|
||||
self.dev_type = "type_0a"
|
||||
self.debug("reset switching to dev_type %s", self.dev_type)
|
||||
return await self.exchange(RESET, dpIds)
|
||||
|
||||
return True
|
||||
|
||||
async def update_dps(self, dps=None):
|
||||
"""
|
||||
Request device to update index.
|
||||
|
@@ -4,14 +4,19 @@ 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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_OPTIONS = "select_options"
|
||||
CONF_OPTIONS_FRIENDLY = "select_options_friendly"
|
||||
from .const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_OPTIONS_FRIENDLY,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
)
|
||||
|
||||
|
||||
def flow_schema(dps):
|
||||
@@ -19,9 +24,14 @@ def flow_schema(dps):
|
||||
return {
|
||||
vol.Required(CONF_OPTIONS): str,
|
||||
vol.Optional(CONF_OPTIONS_FRIENDLY): str,
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocaltuyaSelect(LocalTuyaEntity, SelectEntity):
|
||||
"""Representation of a Tuya Enumeration."""
|
||||
|
||||
@@ -92,9 +102,24 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity):
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
super().status_updated()
|
||||
|
||||
state = self.dps(self._dp_id)
|
||||
self._state_friendly = self._display_options[self._valid_options.index(state)]
|
||||
self._state = state
|
||||
|
||||
# Check that received status update for this entity.
|
||||
if state is not None:
|
||||
try:
|
||||
self._state_friendly = self._display_options[
|
||||
self._valid_options.index(state)
|
||||
]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Friendly value couldn't be mapped
|
||||
self._state_friendly = state
|
||||
|
||||
# Default value is the first option
|
||||
def entity_default_value(self):
|
||||
"""Return the first option as the default value for this entity type."""
|
||||
return self._valid_options[0]
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema)
|
||||
|
@@ -66,5 +66,10 @@ class LocaltuyaSensor(LocalTuyaEntity):
|
||||
state = round(state * scale_factor, DEFAULT_PRECISION)
|
||||
self._state = state
|
||||
|
||||
# No need to restore state for a sensor
|
||||
async def restore_state_when_connected(self):
|
||||
"""Do nothing for a sensor."""
|
||||
return
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSensor, flow_schema)
|
||||
|
@@ -121,7 +121,12 @@
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
"heuristic_action": "Enable heuristic action (optional)",
|
||||
"dps_default_value": "Default value when un-initialised (optional)",
|
||||
"restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection",
|
||||
"min_value": "Minimum Value",
|
||||
"max_value": "Maximum Value",
|
||||
"step_size": "Minimum increment between numbers"
|
||||
}
|
||||
},
|
||||
"yaml_import": {
|
||||
|
@@ -10,9 +10,12 @@ from .const import (
|
||||
ATTR_CURRENT,
|
||||
ATTR_CURRENT_CONSUMPTION,
|
||||
ATTR_VOLTAGE,
|
||||
ATTR_STATE,
|
||||
CONF_CURRENT,
|
||||
CONF_CURRENT_CONSUMPTION,
|
||||
CONF_VOLTAGE,
|
||||
CONF_DEFAULT_VALUE,
|
||||
CONF_RESTORE_ON_RECONNECT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,6 +27,8 @@ def flow_schema(dps):
|
||||
vol.Optional(CONF_CURRENT): vol.In(dps),
|
||||
vol.Optional(CONF_CURRENT_CONSUMPTION): vol.In(dps),
|
||||
vol.Optional(CONF_VOLTAGE): vol.In(dps),
|
||||
vol.Optional(CONF_DEFAULT_VALUE): str,
|
||||
vol.Required(CONF_RESTORE_ON_RECONNECT): bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +64,12 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
|
||||
)
|
||||
if self.has_config(CONF_VOLTAGE):
|
||||
attrs[ATTR_VOLTAGE] = self.dps(self._config[CONF_VOLTAGE]) / 10
|
||||
|
||||
# Store the state
|
||||
if self._state is not None:
|
||||
attrs[ATTR_STATE] = self._state
|
||||
elif self._last_state is not None:
|
||||
attrs[ATTR_STATE] = self._last_state
|
||||
return attrs
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
@@ -69,9 +80,10 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity):
|
||||
"""Turn Tuya switch off."""
|
||||
await self._device.set_dp(False, self._dp_id)
|
||||
|
||||
def status_updated(self):
|
||||
"""Device status was updated."""
|
||||
self._state = self.dps(self._dp_id)
|
||||
# Default value is the "OFF" state
|
||||
def entity_default_value(self):
|
||||
"""Return False as the defaualt value for this entity type."""
|
||||
return False
|
||||
|
||||
|
||||
async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSwitch, flow_schema)
|
||||
|
@@ -96,7 +96,9 @@
|
||||
"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)"
|
||||
"entities": "Entities (uncheck an entity to remove it)",
|
||||
"manual_dps_strings": "Manual DPS to add (separated by commas ',') - used when detection is not working (optional)",
|
||||
"reset_dpids": "DPIDs to send in RESET command (separated by commas ',')- Used when device does not respond to status requests after turning on (optional)"
|
||||
}
|
||||
},
|
||||
"pick_entity_type": {
|
||||
@@ -182,7 +184,12 @@
|
||||
"preset_set": "Presets Set (optional)",
|
||||
"eco_dp": "Eco DP (optional)",
|
||||
"eco_value": "Eco value (optional)",
|
||||
"heuristic_action": "Enable heuristic action (optional)"
|
||||
"heuristic_action": "Enable heuristic action (optional)",
|
||||
"dps_default_value": "Default value when un-initialised (optional)",
|
||||
"restore_on_reconnect": "Restore the last set value in HomeAssistant after a lost connection",
|
||||
"min_value": "Minimum Value",
|
||||
"max_value": "Maximum Value",
|
||||
"step_size": "Minimum increment between numbers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
img/2-device.png
BIN
img/2-device.png
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
7
info.md
7
info.md
@@ -96,10 +96,13 @@ If you have selected one entry, you only need to input the device's Friendly Nam
|
||||
|
||||
Setting the scan interval is optional, it is only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues.
|
||||
|
||||
Setting the 'Manual DPS To Add' is optional, it is only needed if the device doesn't advertise the DPS correctly until the entity has been properly initiailised. This setting can often be avoided by first connecting/initialising the device with the Tuya App, then closing the app and then adding the device in the integration.
|
||||
|
||||
Setting the 'DPIDs to send in RESET command' is optional. It is used when a device doesn't respond to any Tuya commands after a power cycle, but can be connected to (zombie state). The DPids will vary between devices, but typically "18,19,20" is used (and will be the default if none specified). If the wrong entries are added here, then the device may not come out of the zombie state. Typically only sensor DPIDs entered here.
|
||||
|
||||
Once you press "Submit", the connection is tested to check that everything works.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up.
|
||||
After you have defined all the needed entities, leave the "Do not add more entities" checkbox checked: this will complete the procedure.
|
||||
|
Reference in New Issue
Block a user