From f06b8ccb49d12520ad54cc0d89f7322d9272e66e Mon Sep 17 00:00:00 2001 From: DeeSe Date: Thu, 7 Jul 2022 14:07:19 +0200 Subject: [PATCH 01/26] Added DPS value type configuration --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/fan.py | 9 ++++++--- custom_components/localtuya/strings.json | 1 + custom_components/localtuya/translations/en.json | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index c940304..5dea150 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -73,6 +73,7 @@ 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" +CONF_FAN_DPS_TYPE = "fan_dps_type" # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index d2b4583..80743df 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -32,6 +32,7 @@ from .const import ( CONF_FAN_SPEED_CONTROL, CONF_FAN_SPEED_MAX, CONF_FAN_SPEED_MIN, + CONF_FAN_DPS_TYPE ) _LOGGER = logging.getLogger(__name__) @@ -48,6 +49,7 @@ def flow_schema(dps): 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, + vol.Optional(CONF_FAN_DPS_TYPE, default='str'): vol.In(["str", "int"]), } @@ -73,6 +75,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") self._ordered_list_mode = None + self._dps_type = int if self._config.get(CONF_FAN_DPS_TYPE) == "int" else str if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1: self._use_ordered_list = True @@ -138,7 +141,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self.async_turn_on() if self._use_ordered_list: await self._device.set_dp( - str( + self._dps_type( percentage_to_ordered_list_item(self._ordered_list, percentage) ), self._config.get(CONF_FAN_SPEED_CONTROL), @@ -151,7 +154,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): else: await self._device.set_dp( - str( + self._dps_type( math.ceil( percentage_to_ranged_value(self._speed_range, percentage) ) @@ -221,7 +224,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) if current_speed is not None: self._percentage = ordered_list_item_to_percentage( - self._ordered_list, current_speed + self._ordered_list, str(current_speed) ) else: diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 4db7e70..0de4543 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -104,6 +104,7 @@ "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", "fan_direction_reverse": "reverse dps string", + "fan_dps_type": "DP value type", "current_temperature_dp": "Current Temperature", "target_temperature_dp": "Target Temperature", "temperature_step": "Temperature Step (optional)", diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 82f4064..909a69c 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -165,6 +165,7 @@ "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", "fan_direction_reverse": "reverse dps string", + "fan_dps_type": "DP value type", "current_temperature_dp": "Current Temperature", "target_temperature_dp": "Target Temperature", "temperature_step": "Temperature Step (optional)", From 364569bad8bf928b3b3b55cf056bab89bb3b2b59 Mon Sep 17 00:00:00 2001 From: sibowler Date: Wed, 13 Jul 2022 20:35:42 +1000 Subject: [PATCH 02/26] Rebase from upstream --- custom_components/localtuya/binary_sensor.py | 7 ++ custom_components/localtuya/common.py | 117 +++++++++++++++++- custom_components/localtuya/config_flow.py | 22 ++++ custom_components/localtuya/const.py | 16 +++ custom_components/localtuya/cover.py | 1 + custom_components/localtuya/number.py | 26 ++-- custom_components/localtuya/select.py | 32 ++++- custom_components/localtuya/sensor.py | 5 + custom_components/localtuya/switch.py | 17 ++- .../localtuya/translations/en.json | 10 +- 10 files changed, 235 insertions(+), 18 deletions(-) diff --git a/custom_components/localtuya/binary_sensor.py b/custom_components/localtuya/binary_sensor.py index 1a3d28a..0011c2f 100644 --- a/custom_components/localtuya/binary_sensor.py +++ b/custom_components/localtuya/binary_sensor.py @@ -52,6 +52,8 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity): return self._config.get(CONF_DEVICE_CLASS) def status_updated(self): + super().status_updated() + """Device status was updated.""" state = str(self.dps(self._dp_id)).lower() if state == self._config[CONF_STATE_ON].lower(): @@ -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 diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 79eadc9..1dcdee0 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -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,9 @@ from .const import ( DATA_CLOUD, DOMAIN, TUYA_DEVICES, + CONF_DEFAULT_VALUE, + ATTR_STATE, + CONF_RESTORE_ON_RECONNECT, ) _LOGGER = logging.getLogger(__name__) @@ -91,6 +95,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,6 +141,7 @@ 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.set_logger(_LOGGER, self._dev_config_entry[CONF_DEVICE_ID]) @@ -142,6 +149,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 connected(self): """Return if connected to device.""" @@ -173,6 +184,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.status_updated(status) + # Attempt to restore status for all entites 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", @@ -254,7 +269,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 +320,16 @@ 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 +350,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 +363,20 @@ 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 & 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 +449,83 @@ 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 (self._device._connect_task is None): + 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: + # (stored_state.state == "unavailable") | (stored_state.state == "unknown") + # ): + self._last_state = raw_state + self.debug( + "Restoring state for entity: %s - state: %s", + self.name, + str(self._last_state), + ) + + def default_value(self): + """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): + """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): + """Returns 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) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 695baa9..203c0c6 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -44,6 +44,7 @@ from .const import ( DATA_DISCOVERY, DOMAIN, PLATFORMS, + CONF_MANUAL_DPS, ) from .discovery import discover @@ -88,6 +89,7 @@ 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, } ) @@ -99,6 +101,7 @@ 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, } ) @@ -140,6 +143,7 @@ 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.Required( CONF_ENTITIES, description={"suggested_value": entity_names} ): cv.multi_select(entity_names), @@ -240,6 +244,22 @@ async def validate_input(hass: core.HomeAssistant, data): ) detected_dps = await interface.detect_available_dps() + + # if manual DPs are set, merge these. + _LOGGER.debug("Detected DPS: %s", detected_dps) + if CONF_MANUAL_DPS in data: + + manual_dps_list = 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: + # trim off any whitespace + new_dps = new_dps.strip() + 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 +273,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) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index c940304..993a1f1 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -35,11 +35,14 @@ 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" # light CONF_BRIGHTNESS_LOWER = "brightness_lower" @@ -113,3 +116,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" diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 2a3eb8b..d8e426f 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -189,6 +189,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): self.debug("Restored cover position %s", self._current_cover_position) def status_updated(self): + super.status_updated(self) """Device status was updated.""" self._previous_state = self._state self._state = self.dps(self._dp_id) diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 596eb01..eae033f 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -5,13 +5,19 @@ 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.helpers.restore_state import RestoreEntity from .common import LocalTuyaEntity, async_setup_entry _LOGGER = logging.getLogger(__name__) -CONF_MIN_VALUE = "min_value" -CONF_MAX_VALUE = "max_value" +from .const import ( + CONF_DEFAULT_VALUE, + CONF_MIN_VALUE, + CONF_MAX_VALUE, + CONF_DEFAULT_VALUE, + CONF_RESTORE_ON_RECONNECT, +) DEFAULT_MIN = 0 DEFAULT_MAX = 100000 @@ -28,10 +34,12 @@ def flow_schema(dps): vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), ), + vol.Optional(CONF_DEFAULT_VALUE): str, + vol.Required(CONF_RESTORE_ON_RECONNECT): bool, } -class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity, RestoreEntity): """Representation of a Tuya Number.""" def __init__( @@ -51,6 +59,11 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): self._max_value = self._config.get(CONF_MAX_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 value(self) -> float: """Return sensor state.""" @@ -75,10 +88,9 @@ 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 self._min_value async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 29d11c9..71f98cb 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -4,14 +4,21 @@ 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,6 +26,8 @@ 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, } @@ -91,10 +100,23 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): await self._device.set_dp(option_value, self._dp_id) def status_updated(self): + super().status_updated() """Device status was 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 self._valid_options[0] async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema) diff --git a/custom_components/localtuya/sensor.py b/custom_components/localtuya/sensor.py index c8b2ddb..e769f4e 100644 --- a/custom_components/localtuya/sensor.py +++ b/custom_components/localtuya/sensor.py @@ -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) diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index e884095..c448ff5 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -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,9 @@ 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 async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSwitch, flow_schema) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 82f4064..a97f120 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -96,7 +96,8 @@ "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)" } }, "pick_entity_type": { @@ -181,7 +182,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" } } } From 0fe550576a306f9f93e356554628f4fdd9e7a8fd Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 06:29:56 +1000 Subject: [PATCH 03/26] Fix NumberEntity HA changes and add step control --- custom_components/localtuya/common.py | 6 ++--- custom_components/localtuya/number.py | 32 +++++++++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 1dcdee0..0e7fc8b 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -323,10 +323,10 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): self._state = None self._last_state = None - #Default value is available to be provided by Platform entities if required + # 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 + # 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 ) @@ -366,7 +366,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): @property def extra_state_attributes(self): """Return entity specific state attributes to be saved & then available for restore - when the entity is restored at startup. + when the entity is restored at startup. """ attributes = {} if self._state is not None: diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index eae033f..85974e8 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -5,7 +5,6 @@ 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.helpers.restore_state import RestoreEntity from .common import LocalTuyaEntity, async_setup_entry @@ -17,10 +16,12 @@ from .const import ( CONF_MAX_VALUE, CONF_DEFAULT_VALUE, CONF_RESTORE_ON_RECONNECT, + CONF_STEPSIZE_VALUE, ) DEFAULT_MIN = 0 DEFAULT_MAX = 100000 +DEFAULT_STEP = 1.0 def flow_schema(dps): @@ -34,12 +35,16 @@ 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, } -class LocaltuyaNumber(LocalTuyaEntity, NumberEntity, RestoreEntity): +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): """Representation of a Tuya Number.""" def __init__( @@ -57,34 +62,47 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity, RestoreEntity): 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) + 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 value(self) -> float: + def native_value(self) -> float: """Return sensor state.""" return self._state @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return self._min_value @property - def max_value(self) -> float: + def native_max_value(self) -> float: """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.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await self._device.set_dp(value, self._dp_id) From 06968d8a9c8456256cc932da81ff3de6e57b6c74 Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 06:58:00 +1000 Subject: [PATCH 04/26] Rebase merge fix. Earlier commit also included changes for manually specifying DPS where entity detection failed, restoring DPS values on reconnect and also supporting devices which don't report DPS status until it has been set once. --- custom_components/localtuya/strings.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 4db7e70..a669d56 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -120,7 +120,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": { From f85b25dea903287cf1efd35e3935fab076c6ff6e Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 08:59:54 +1000 Subject: [PATCH 05/26] Fixing formatting/style errors --- custom_components/localtuya/binary_sensor.py | 4 +- custom_components/localtuya/common.py | 45 +++++++++++++------- custom_components/localtuya/cover.py | 3 +- custom_components/localtuya/number.py | 10 ++--- custom_components/localtuya/select.py | 9 ++-- custom_components/localtuya/sensor.py | 2 +- custom_components/localtuya/switch.py | 1 + 7 files changed, 46 insertions(+), 28 deletions(-) diff --git a/custom_components/localtuya/binary_sensor.py b/custom_components/localtuya/binary_sensor.py index 0011c2f..273880c 100644 --- a/custom_components/localtuya/binary_sensor.py +++ b/custom_components/localtuya/binary_sensor.py @@ -52,9 +52,9 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity): return self._config.get(CONF_DEVICE_CLASS) def status_updated(self): + """Device status was updated.""" super().status_updated() - """Device status was updated.""" state = str(self.dps(self._dp_id)).lower() if state == self._config[CONF_STATE_ON].lower(): self._is_on = True @@ -67,7 +67,7 @@ class LocaltuyaBinarySensor(LocalTuyaEntity, BinarySensorEntity): # No need to restore state for a sensor async def restore_state_when_connected(self): - """Do nothing for a sensor""" + """Do nothing for a sensor.""" return diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 0e7fc8b..38a5f40 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -95,7 +95,7 @@ async def async_setup_entry( entity_config[CONF_ID], ) ) - #Once the entities have been created, add to the TuyaDevice instance + # Once the entities have been created, add to the TuyaDevice instance tuyainterface.add_entities(entities) async_add_entities(entities) @@ -150,9 +150,14 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.dps_to_request[entity[CONF_ID]] = None def add_entities(self, entities): - """Set the entities associated with this device""" + """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.""" @@ -184,7 +189,8 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.status_updated(status) - # Attempt to restore status for all entites that need to first set the DPS value before the device will respond with status. + # Attempt to restore status for all entites 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() @@ -326,7 +332,8 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): # 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 + """ 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 ) @@ -365,8 +372,10 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): @property def extra_state_attributes(self): - """Return entity specific state attributes to be saved & then available for restore - when the entity is restored at startup. + """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: @@ -454,7 +463,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): # 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 (self._device._connect_task is None): + if (state is not None) and (not self._device.is_connecting): self._last_state = state def status_restored(self, stored_state): @@ -464,8 +473,6 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): """ raw_state = stored_state.attributes.get(ATTR_STATE) if raw_state is not None: - # (stored_state.state == "unavailable") | (stored_state.state == "unknown") - # ): self._last_state = raw_state self.debug( "Restoring state for entity: %s - state: %s", @@ -474,7 +481,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): ) def default_value(self): - """Default value of this entity + """Return default value of this entity. Override in subclasses to specify the default value for the entity. """ @@ -484,8 +491,8 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): return self._default_value - def entity_default_value(self): - """Default value of the entity type + 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. """ @@ -493,14 +500,22 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): @property def restore_on_reconnect(self): - """Returns whether the last state should be restored on a reconnect - useful where the device loses settings if powered off""" + """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""" + """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", + "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, ) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index d8e426f..b9c10f7 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -189,8 +189,9 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): self.debug("Restored cover position %s", self._current_cover_position) def status_updated(self): - super.status_updated(self) """Device status was updated.""" + super.status_updated(self) + self._previous_state = self._state self._state = self.dps(self._dp_id) if self._state.isupper(): diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 85974e8..e8ab53d 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -8,10 +8,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, STATE_UNKNOWN from .common import LocalTuyaEntity, async_setup_entry -_LOGGER = logging.getLogger(__name__) - from .const import ( - CONF_DEFAULT_VALUE, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_DEFAULT_VALUE, @@ -19,6 +16,8 @@ from .const import ( CONF_STEPSIZE_VALUE, ) +_LOGGER = logging.getLogger(__name__) + DEFAULT_MIN = 0 DEFAULT_MAX = 100000 DEFAULT_STEP = 1.0 @@ -66,16 +65,14 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): 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 + # 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: @@ -108,6 +105,7 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): # 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 diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 71f98cb..75dd217 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -11,8 +11,6 @@ from homeassistant.const import ( from .common import LocalTuyaEntity, async_setup_entry -_LOGGER = logging.getLogger(__name__) - from .const import ( CONF_OPTIONS, CONF_OPTIONS_FRIENDLY, @@ -31,6 +29,9 @@ def flow_schema(dps): } +_LOGGER = logging.getLogger(__name__) + + class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): """Representation of a Tuya Enumeration.""" @@ -100,8 +101,9 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): await self._device.set_dp(option_value, self._dp_id) def status_updated(self): - super().status_updated() """Device status was updated.""" + super().status_updated() + state = self.dps(self._dp_id) # Check that received status update for this entity. @@ -116,6 +118,7 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): # 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] diff --git a/custom_components/localtuya/sensor.py b/custom_components/localtuya/sensor.py index e769f4e..0eb0ae4 100644 --- a/custom_components/localtuya/sensor.py +++ b/custom_components/localtuya/sensor.py @@ -68,7 +68,7 @@ class LocaltuyaSensor(LocalTuyaEntity): # No need to restore state for a sensor async def restore_state_when_connected(self): - """Do nothing for a sensor""" + """Do nothing for a sensor.""" return diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index c448ff5..40e8ea1 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -82,6 +82,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): # Default value is the "OFF" state def entity_default_value(self): + """Return False as the defaualt value for this entity type.""" return False From 41b5be03a3775fe77baf45dd98f609a535aefc9e Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 11:41:23 +1000 Subject: [PATCH 06/26] Adding notes to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 01cbba9..2201253 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ 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. + Once you press "Submit", the connection is tested to check that everything works. ![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) From 1c9a1e4e4bc1792f6ae759041ad4e30a310bc098 Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 15:26:58 +1000 Subject: [PATCH 07/26] Spelling correction --- custom_components/localtuya/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 38a5f40..38401bc 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -189,7 +189,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.status_updated(status) - # Attempt to restore status for all entites that need to first set + # 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() From 489a6f09feb82aad01768277e04ad7aef50c0f59 Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 14 Jul 2022 18:59:38 +1000 Subject: [PATCH 08/26] Updating documentation --- README.md | 2 +- img/2-device.png | Bin 27833 -> 27806 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2201253..756347f 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Setting the 'Manual DPS To Add' is optional, it is only needed if the device doe Once you press "Submit", the connection is tested to check that everything works. -![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) +![image](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) 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. diff --git a/img/2-device.png b/img/2-device.png index b82b2353c2e99631ef26c40eedd272f544a24db6..335d787033860e2b249bf04698cdce77d8afba8c 100644 GIT binary patch literal 27806 zcmd43byytTo-Ld}0s#^vxW6GGg1buy?(PnuacH!00t9yn+CYE=3GVLFA<(!6ZLD#3 zy~X>UxijCK`Odv_?w$F*KX~Y;x@z~Xs=e1-wdGO$2 z(t`)6kDs6cTk13$ZGa7dmZG9c(xRe&fF12YmNsS&9=sj=5-TXxsYDQ{sQR6b5IsEP z?MI7rhLA8G8G>-5tSUks7Z&%8=%H^fq#Yax^sumR88j&Mpe`<8!bk1YxNjuih!ej% zX!$loqqg6^>J2>`K(t&v$kH%L9cRV1Xs~^48kgK#65^-nZ2Fnb|6z)@gg|wS#LS5c z4)wxbwgA?hZnM>Z$tT5E8D>G2_@(%Y_P^1P-b=XjzvvEl50a&yJr{P=KRAzcYB%#U z59)VzjN%bvAkxvYG}hqYTza=u_G2S~`J;dauf4;B4B^Cjd@D=xXp*X49+||`@Q{}e z@i6J8-eYiOS8aXMk{YQW>k(eL#9vYQS^ZEV$~_@V#Pw#;ci*!{!^g2h7E|UuGJM%f z>{A<)QYqiSS^4sq;)6SO@OSQ;RWg<%X^-lH!01mcwu?&*1@AoDRM(xSm$={*G~|auZ(c5H*VahkT_@!$WLrzhM-!(rR~z=GN?O)A9h$2u{3)V--ht zqZDvoFu+pUP7fYncisOz{6ddS4s1j-k(Cq!{=a_^NjB6=1AIbvmX;SsU%`Axj887I zTkZ*LB6Swmau&6>wKcPIejw^-X6$VC@ehclv&A2h((+20e%Qnh9$2|Zi;1YY8SKnq zK;>7L(e{x8$t=CkWnOW{RfxaL`usDYI>AEyTV$M^c%jA#ccfaC)^Fc$Z?&y3V`;y< z^L!N`Mkjk})<1jaKJPhD=LG-7Zwm5gX&t*HYJUZw$Ya)K^ zd)gBk^79~Gxt`*Bntsr!gZ@mgy>rF$?&fT?$Bq1_f&}^CMup_d(g}JRm8q3n*I05r zdx`$(=lxmtmWcsmUZO^S&G3w<(af7B3BE=$zDL`6@w5^yBc1jNGsvk$*z zWL$5G;57|R9C<%>Y21>urjv59xR{t@UtiRsK<~ctM_}jKCj+?HX<+5DkdU9{m;=v8 z86+@C@bGwb+O<29te9wMOvJ&7-^TUC#KfY;@i>#9x1MakPiK9&D=v4ArU?%TpL$fXx9LZSh*i-9B*i z&*3KnF;yVeo5=g73Ktoa?_1KsgTA@sCkFZf%F>RTKxTu1uI3OzUr;FgYx~b#)`4RP zC7tj8O)ZWajC6=s-yfr65cJKO=qOG?Ol-yC^O{t$6$5Y{;Ysf-If1WZO=C;emRf*g zwAy1X6mRj&97o({z1;=JBEmkXsnI{xhL>NJ*)B`i%$S2>y~llka zMSp;MBz-l#yE9fdAl~g|$MrmU@TzD!axBiw;jq^XcO$|;@S7kRmvvB(zJ!sf@z3c{ z!G4Fn({Zmd)z5dt@)CRNxfT;u?@Y|=a=uCF)Yy<_c%M#J;Z9ds#`8Z0O<_?Ebh^{- zJwo<6SP;qfjI?WJYeyR=?-1<2b*WHoOG0tA@2F6u7QO1Df2d`(eZkenIHsATYq~Rk6+Ob^_*nB^;0Ke=HmI=JypEdALm|Ds2uY}1^qKvBUZ9TNJ_L)a9R2JKCLEnP*th+qg~e$t*8Bzc zI>=PQa12r5;_ThU-bHDSl=|i;L*hkP6)P>o<$pY+J8!{XGpgaBwzDz z`7sufF<&HQkn(b2X(q6UJ3+LtX4p?zghog3lkuQ%G?Ll|y_D8;tE&+lX!(Kv(^8Ys zFQU#CP7JrA8`?V0Lu?3><}Yrysu^o7*RQXBB`}8WuP~lFd{mshYH!`oVc4vyqp;mn zd)iO#{yMeFGJq>`;E9(IO4?pSHu5KR*)%VVwl5Jh z{v|e~f>C!2JudsAD#MHliaED+0EkmI<>9RC}Ian>@vCO^M!ls zYkU@~)$v%aQ>IhRUEn zZx5nLFZYip@R;l3`XYl+&UdqZDm(?wMrwR!n_q2f?bauDOw^W}e;bvh}<+FVhl1QlW2)S1IgJdjnERv}QkU zyJE`Z>g!dv<7dbt{b#6|qB|tf?e=dFG1I3ItsWO9?I3AEHmn?|jL64MNfu*-mm5=Bb8MIHtTBtK`OfoTO8b_ z0%f7!RM+)YDx|6Z-Guof95Tu0l+ej(O0*abS!ioC^a_-$&v{3`KRiX4Lev!6X&{-c zdV0jKz~QYgY&;A#A7-cupl78TR9ILg*kr`z_6_AF5@z(Eo`ZV&ClJ2G-GlxL>Ep}h z9X0D@kTcw~b7+_ApWAw>86g6iUZONtYeL(*ZkiGtaIo0W3vUG7PzR-iTcQhjtuYqK zW5BeVU!#?L50quO3$@78XvT^+qzaTz@s{o--u^n6AN}3qWOFS&{R}!l92^=5eyhN2 zQB9175ImcWw`}{2hINl$&jNZr%ifOZQ1k{vJNSrOpdqDNMrtiYXOI%ybyaX!T|+re z)-&pZOLMx{6n4T+VmUW12CBM`QaObdo*KhTMId#&$2+wwUy6ooAy$zunMY^yBdn?G zDW5?bjm5-xtY`fcg|Ft1QO8>^?8XFNun_-R2(eBB7t)s*+1+yVaO~ouY8a^afUO=z zgc8Q@(^?b7!iQtWiK;rv%%r7cWV&KN1_2;<4lAW;ip*yNaM7NP3W8D4t!Yr|ENOVd zZZ_VR&E?rczn5FuEDm%$=Jl?SuTB?jJYpO@Cp+$AXEYACD##iymTS0N-5qHMSffgdU@NL< zsG_0K<#Rb->{Et$5P6xW>-c)hJQ_(@PZaEsc(*08N5!%~0 z^=(^Dp&zbdTg^w+s>rP2?mxngHr?odP36M<{Ee^b$Y+;v-CR8Q!P(r!tK}hWW|S!M zrt~crWc=4@EY`PZ=F}B-&vk3UvJkTfJ|u@$XQKg(T!FF1&?%3s`atoKf+S6b9oX$k znG@ZpgtV1P`ofv-l_`Us(=Ti6zM`PGG?&%92$WowF)jb^H-$ehR6GkDn0^EwMLBT0 z?mUffLHe_84-Z#omp!u!zByYte3C$)NS zalYNYJGhoZGTONE;3_n7Xn7cU01(*adC&&GtZj%urF0OiW9wyjU6JTlOPk@^kpGu48ea&I{@x!MxRbnma*u6>$s9CzTcDbUNimJ4)Ue%e7(&r;SWIa?J3ZUhX zJ**eKQ{gBWJljcT-T4WNj5qJf*?Sqi7;6p-AY9OKvFi9Zj0e~8HL2Y2W(<~rO-Otx zAVX>lew?ZKBAyaIo27xe&v)hb!61NdWjLw-(>rYa=`KLSlb=8sMrYvio zT*LbQ=Af2HKf!6?iD#_Hj1?|&(3;*@{;xB6g-DjI*DY*s%9}?W<{8~M159HlB-c12 zzh8e!8T1Mlf33?0`u(a$GJb39sT%3zb?R+INhXXu)#2P~l?wSPL1fW6LA=n9)aEWy zWL=4N%bp%)SP6oI5rM?Hg1;Mm?|dL(D_@45_AY=}6WSvouJ0|8wHJd^!TWM1L-Dw} z$i-fSlU$lx_dxAeLYO{T0EoewE22{tzqU@Expzun^O@Z8OF61Vv!^GPh*(9SyiLJe$v}Qm_X!#FO|=0*Da`|gVVz2PDNJA z?>d~Ql<8X6lxuEgR}-Phal@JToVyXOgz5NI#*9;El=DS`O*Xu<<~3OS&8f&1F}Y;C z^|POPj8&EcdB-X&=57>;4OcNu?syt@@~tc1HMzL&OgiREZ*iC(=~MSg=YV1IFGmsu zXE|z!4dpUci|T1i_(1x$^;PboNBLJlgY)ear>ny)yd?8?$^wT!E0Y@z#E{NaqKnQ& zJ;`}JrcS?4YfJ`Sa%b85Ll}P^bTpoAvuK4Vel{ay;MJZyc>Wug=82+qcx~gD=nfWHwsEINzPm9b!YMPE?4(XtDq$ny&hrPY41eHs5ju0v z#j|vG=KN^T;2Rp+CJGa(t<3K9O;$Q5O0;>RK$90Uy8>~FvX2V`6*QBb+n11OXlsR< z=;;!N(v{ExS49es^i=}SMv(F1_h(?gX$@PBT_K_)koTtz-v)N7VvU_)f7;VijCJKZ z6r`Ksr{9sLtl@Vm?ReS4thw2`_!xH2oyImlpUx^W^c=}=@org$c$_pY?tI&BwRBzx z9_}c3{;h~IbYNF#%>I;Qjps|C5gw}|Vxui$;6$1F#Ek*Em7}nQWWBkV~7<>MPtP_lXjH>v?unrB2$MfvX{*WPcKtcI~)9PGY*~6yj8?zZ)k%+%u1eUab)!6XTOoUty;7!AZoeFuy!nvH{S8!p+OR1nfYa&CrV(7f>h>d_%`-M3f&hQgd{FPzmdN@UXX1}O`Tp*r+n)^oFx zM*g@Vo^hr8uo_jmm?b;A@gkeQ=hK%YXL%C|Mgbn5rylxlbTYZSJvEe9V3QjIk{hY; zwg571&KHS$5!}Mw4_U{Xyx#`#3C_3U7mHFKyp0|(1uex(nkc2;(cwmSM-f2vkXA^j zV*B#2$RJamAm`dgSvho$fn0}y?N%qKVO;|gfYXEZyEVME*o0u?(&6(%xHXg!U2Kdp zgc;wUTYlh70P(f5bim8AV^Hj<#T05?PEKcDOWLbyp6Ok~pHM$FrFFET<;p$avh~|r z4;Iv)f!UWI-}=X-kr#98bw%c#198S@bSk$tO2&u1QVLOv_?^KA%oCpSJ@bxp`>kEb zbd=&JGG5OkOFgM|(_qHk$pgXGOf-)l-Mweyj;1jG>{~quOTvol_ovlTSmUKyT3$vR zUEmTC1wE&gq43d>i>h)lp>_^S_ph>xS%IG~G_s@kme1F@&vW)tYU;AyehEFFp^geF z)_JG(?ftKH=G}nq?(P&{yUaj+P=zk+fCbc_Y$N)z3a!%&^EtC>lxU@vaXHy?Pk+y^ z-ZR7qn*7i~LzIO|D=AglmYA%V{_L5OTr9m?>$>j?vI*<3hucpIrXG8j_Vs}zV{>4V z5f@@p!%G!r;zL#+u?l~Zy$*iiBtz#{~YDLnZadun(Al5%rF(B|3HF4Uwj9?6< zp2f71q+B}!kq5?${SfKvJs8tlf)nJ;@brtnmgzgp51q8vVe};=U0iG}sa&>JufswJ zi1MJ0m`u&bDu90TxI64|hR|#^{XSIiiq>kPC^R{lM9*`l8f4H*euNz4_SW+Pq#BBn zdKSI&^^A}ZDLZ!@B{xpuKQ#{*OhCsdS3m%o%EUWBn(5-hXS$^K2BZ&-cJ1ucduapc z7aragpoqwu($F|oeP9>6*FOFq#{~ZCegyYIPkIONi$(Y9O~~K$CBTP-G#ou6^^?3`bbZC{QM+5y9=IPOj7-HhfNKvS z%`8)rCcLAFGuFcqN3$$U7EqS3uuX)R*Z^hU$saVZL5G5RwH;1UX=&*V15-ZqT>Xnn zx}pqcbUJjQHJQjO{4$RNS&Lnt+ra_{;g)L`Rs$9E%EW~bt~Zc5H0iSm8=$dhMVX*b8f6S|dn4N+RD#+51BH8)Z=;9KqrQ5O#CrtAH= zyR24L+8SCb^NOfqjOZNGoYY)2jY?(Br@D2M>2a~eQsC1!w)z3Bovsj>)M5;`Y}J=z zAB&@op2~I!MHvb|Wql!U!9Ty@pC6vl_Mv9R9Uf%KS^Du+WGgwN^`z8HZM##`>(??p zBdoOuHuh#TtJ>}8USBHbJhQgx(pyIBoS@p0y_(>$n7S6KjR|fur$wohBQpLz($qk$ z+FYwdd4-O1&);7SXW3o&_}NEYc-Kf(7nXWg6-O5V4>G?}dQ5rZ737+~B8>3?1o_W7LA=+nUlNsVnH z7}RrOUgPw7EL@}$D30&(hs#IEaYh|#{>0;SE8DiD_$KFRzjqknK%Ib}&7NPloji-K zDSS!KL>?caF5P!^jhb&WsOT5Y#Ry+t!tf;O*zggf`#mQ zvoxy+do@__`dsI~alJ{f8%#f$iy{wtVd0JCU^vNxf0_>C;T)?Y8c@ROs^?VSJ=pe3 zKbZOfa@z`4m(t-Rk4|Sr^}h64_o{>@NBVKMM);OcL@>C92 zk`8W5tPi$LiL>Oy9?4ABW8Lr*!Jy3PgxPE*R=QZ^AnzT56R0~Hh(UmqH9Z@Z)`gFM zf8%OYxqpT`K@2`)*oLp)XEbT0VCYvHG}!Ha6SNd$75l!IeQ2#*7w-n@)U5=@ z%j87xlf?Lo);E{Kohb{;t>iIlq8nelWw39XvJGaN?vW{)#|?GbQgagKrwPkS=9ene zEEf57>QGmHi9n~eBdMi)w~I+{I8Icp;Tu_?0DtGc_VgO;_>`@m$fhgP7qxneAAeM> zlXI}euaV3#`3mKg(A9a3|rsMpdJfLPZMHDpG>xn%2I9CU|1`XZf&W0P~t`SdqT(0zU8-{eIC3gG*^1tLOi?k z=c^)&d4&<1r$ydXp1VbR#hj8QrHT9(Pxx8N2dmv2)!8GGTLoBr^<*JB3JD^G?F&mt z(w7Hq-EWqVwH{gO%L;CNJ*IOsj&cM{uy2?H`4my=PA$@tk}*)|wD(zkzOoCm1`~Uv z&coh;^k#l3^@03|eRZdOc+}V2cjn%~lOz^ove3)7Lg~5`#lJyBjOyUu@7fNXZ9nNA zjIKB&n{ydH%hQ|kn)F!rE0Nv0PG3Rb)3$sw{&IVy zju#m}!9u-iLBEUO@M#w{%gXMsR{i8V8wgWpzAjnLa-sDcFVwC6Ku3&O<~@qZPbdsQQwe1+x!oTIsZt7eaZduhgo-;W4X%H3Ji}nSkv$cx!^aJ!iG8 zpsQlb60dG*@cAKZF2Az9roq=Nr}EcLF61>g0+@n#%-p`;t4OGY9B|MgbTCdUwy(U2 zo^uD~*tj1(j)ig%=D==Opvri?_LxaPIk4`d&)dH{^Wd3j>%g$#s$^A|Xa{|9`RbRb zd`flvVSQn-6m-MRbY)U*M)gLde$#{pdys)Xbv5&Ln8Oz)5lToZ-Y}+U3q{a`uAz7y zpK?Cw93ge0H%aFnUtfB=@|smuVW$5GD`l)@4bLxUw`Z9`5NyzChy*;}WBu9~CgTZBGEMgK8Kh}Uvr+<;I{|tfp ziy!^P%Kn#Z>;G_PW+9>Z%+yrO)`e)GJb*aQBWmK}<74CEmZAXrs|lBiDk>!8$>+dT zZ$TO+CTyY1K^uBDHa1^trl*O3jYY2|CU*F+PbaaS!ShJBB?!wJu+QRJqa>%(N)g#) z;UQAD{8VP8e8=Jd@%(460j7RkLdp{C4f)M(-lvg)x+V>X#Js(z*th(*37knSvVN!+ z$IMa-jZ@LJzUVT9s2S(o(PvzB_+nzOR0G-cQZ*l3@&x+izf1GmG`x_P589)Lp!ZZx zICOpxR>;_t47BuVYMF@sJ-{sxyn7mU<18&f4yGFs}&x-+W6@Qt=~Xi z0oqHNxbzRCOr;Y?#=i~Qu8TLYF1;0`yheTor|)>zRg)!d0j%W2ZBR z{u55N_jz71PIsqYG#5(gE8glX0^!@Hk>Ef^FE?00anba$x=z*e1q>{ z7qgsUAyObrv$_O`zX{ludRHr<#l1+zLAc?TBtQ14_A?61Um4_umu*@W>UjBjC>BaY zZ+E?}D)8P{#Q$M~CcnOYGGt?z&EzeHM;v`)hngx{hDDj&(*5Er2E5oBn~4$!4C+Z2*Bw>YRa7 zd%Gg}h52U6`N5R;&-577Xr3+J)(9MP-&L5U;rYRGs}NKvKROC<$CtySqtEhkb369V zUUG3wP^NRA?#^vthlWV`ghfO+2Omo4=}~wIQFnB7^w&7s1GUKCs+zxQiu{hiyArKS z;WPrgSbwSiqcY|n4%a|BJ=dzUb|f}1!rQ<9n=0ymbG{<~?lft7_WTwoiGV19(|QB` zTvij_-$Ti&H5@3r!d~B7?5_s@zXX<+^K&;}D=WHdAK^c-&pmiXMnOSBMkb6OaMBMy zxViBF*oRA$cy%hBgSGZr#Wvjjf{dxC94K3F z`KZk7TY|#xE%=K)w}j@+A)pT3`ro{y|2$sO67M#j8C;()NAKnx&$}%@lS}33y?I4* zxS+A#VK($t&KtCPI(KZ zhQbll!t?3R_knH88+n-uX;gYxe9r5*646w)Jx-TSgMz_xjgUrFr>HIWwG?wXRnlO~ z)9vZ?h1OPW=GwuVR})?SXAUQuv6L$Q_EL+sI08)cF;|%T&%;%UMvwwMk3%=M#@a15<^%3zPLbUIxMz5 zoON4{R$!#=?}MFpXQv4+^U__(N}5k~wij9#7#6Zb(N!BmA|g0O zb8>RZL2$D38y|MTH(K~6@TJ?UJxMXK&!kB)F`t0gK-vw$N8kHoe5%2Pp38ndBAmHp z>Z$!~-Ee;&xWKSg$V({m0-Bl{mg#fZyM1(haRfI9V4y@Q5X+iUy-1b;AJP@_fBV5~>DDFJo7?qbwcOH@6 z)iRo?u{{R{37qf3M3wmaw{OX0nAUW%i43e)u)U!&V^Q|>L?->P*Yc@f&H57aHA|@y znGJUp@bKQ%g?R-FS+ntWzh)uY-DY+_8P{~^W2~)Hbp#;?((7(cXIR}2KW_i7?xQ3J zBhw+NE@-Vk)Cx>;%X2%x#xZX4CRAv$c?#+M`|{j#pTn6Negcs%f{)^KdRi>@Q)MrT z@zZZFx0pW%2B!EVJFfmhL<`@7^BxuHHxjkpUKWyq*vAPq_54)6%cKmpZ}nnKFboLa zd5v-(B7&(KOpUEr+i&Oyns$MBO4^Odkv=#+IYi1>9T;Wl%iZC`us4}rCREdAhG>~x zOLM|k!c((dx%r{^Z&EvM28Hjswn|&=Qu{-)I((i9z36#el5^vk3T&fly*W+8z_OZe zs&@ysVUbKIWwIMNVq{J*byLNfb9HdtI9~eKJP#O?C(_^;}eNHPU+((0J7^sR;F&nW z5qdC;M05t{=r_9NYn5m7YlwXVtJYDNH+!6Plo@vp0dXxzM2OVAPWayddw<9LNcf!d z0Z5~{0H%`9uF^b!BLRa*Q7tVJnwKw+xPdk2n+4`G;y;sGe<%9=!`1TdB*=f33I4Sd z{9Vugf5=(?Q;QZraG#QKJc)lT?!;{XM90#nfei2J3UDN&eP^V86Xf3afXbzUEO zK_TEabVGo9sVydk5@u6Zw&;6vGC5ppZ+NuCg4)rE&@^zTwdD)0e`@`nxoO`&C3w$( z(X!qDvDpArm(204T{_SPMqNa)i+N`SlaqKAwIdXO!Zxleucit2th^1UaaV2>Xc<}{+J7J zG!KAo=PG8}U5fb|QXq|6f7-X+1ntf>zGPz?Y1(bMvST0Ek~H3VNk>O#%ZIcb`s$aG zm|{Cqld@XcbSP8VT*gvO7Z&pIn^IyF0LeXtr7fwu-3}SRx|Ib8;rU8g{`_d5ILO~xXY8RG|Q9=V8xY_&jnfp-S}P~1oH!Q$^RzLt~owFe(uFr zAZm2Ka1EE}^B3vX4t(y9AZ4E|AZ^_IQ8@HX8lQa@SiYgastf|Eni2XqgbJ|Wjc#}Q z5a9%?s;KmHqzd~Ax$gW9Twk{s&GPLg)HE7^@`6%3$N!l7fz%uVAS9m#UCK@({g~4~( zBn5KGX3sOOJ+G$ZXaaVz-G(hD_7bgfliZ6fAUntP5SX9!f6tNCuyFBsh0fJ%48p?0 z*%^hr!V+gL74P+bsavK0P^ABBzM5r_IR)P<`O5#{PIP~^knxXN_@37da8bp#e>w|) zPV4U=R)E~${~bd#T_4KYjR^@^$@96ntQU2E$;8B@WQu_$n!;vw_#dH*3?&c*jeGOw zOag}g|NQ)y_t0=Mp-XsgMP&X1E8?F2X8%(IQ7&_fh*(-%h^`{Gr;;^kM|PmegwYsOImX+T8+J#JUfr*QtT`)oFLk3(UG24k1W@1A5w)Q}k6zKz8Uu_Ml)OhZFjH#ST6OYhlF4;%-qwl&+T#uo z2rP)qZo)fNv`KvkB~#rZN(3c8kx%PF1U08s!Ugs{Z!1No_QJJqItWSu$bAf9s8A9B zZI7-_cRGJpTf>oiEmzp8c?#Waey9f{vdmdP#LDerHAy~;l9DnKU>no@lEOE~_%caM zk@@*=A^paxxpM7I?nn1oQWIdb^B|#4a_}IMkH824%|Ux`lUYrBSjWF=VO;?yH)6$b zHV4VXU<#V@Lq#8IgxIqyh<*D?{~Yn0l+<#*`M30k4=BHD?V~XX=@viZE3}Pk8b$)B zxK!GD+Yv}?U=cLk`k^n8u@hk2_CccMP7At<}QE1w1dydGW#a6P409kc|U0q~DaTgWYd6ml&Cc&AjN z$BX}{sJ!g-&mf@E&nMPT%`@C{uP|PV*|pxdRz}^|6C3kf=O&E7Gi8B@?zM(Ay9NyI zdAFB7Hz%6+^gf;LzC;QIAEd_KQ@Qz#0scLX^wsuKI`tpPme0sI+5qR-{|milw%frH zDOc#~BT+hMa>$%Z5jx|l-~UT0R3H{WXRx0@5k5UFz$GS zkG);EC~(bBgoA2)b9u6cXEa!%-zdizqL^8J+L_l`cMHtM1)JzVp(~9k4niU#kkxyr z?^0K2GOyF?9~#A0R8)&qN_rVM*lzMp*ZtTYEl2mOVX}*6iQcpvFD(}{Yk~GqMi&2Dr z{_IPatzQco&^sNkyOX_cIH#@JznBFi!;38*?r`z1&fn4MZX$g`ZjnwAH(V@<^73$M ziik$?aWS={b}hSKYu-{6)2M)v47L*BRBBwcC8->f&mjyL7Wg43VfTP5!iKs^j*Uyv zog%%CDlbv3Qdv?UqimcZ4zxw_bociY&N&^mzDAfSTm+K((*1})V7@oqpc0QyDC1KM z=f@HU^Qyvg^pjTmLGDN`mp^>&XLH{Z_^U?2?dsA3WBvyON4LME=Km=3|C-B z+skuo@}bNl2=RK0QD%@8h{}z(w?DPKu9WSe%d?;Cz4Q1>d~NAV$eM8rHE2!x&00;t zNb-H`zMnd%to)Xk9_nVr7MO7$95Gw-l$FN;;BnMZhqX_oWek?QEiJ7yLamLARMLes z#ceTL9!9gFXUkiw(psG6JgL{0Z|&S%U&w{49HKmJ*nB7_?DU{TC-L*D;X*WK=0apm zG{aA0Ck>7U*B@a(v|x5R&ivtPxf^s&sCTKEkbU=g#-wR-DRC~NYYnUETe8>l(RR#? zFvR76p4Xu&c?6UT-9hv4$mVIC<=dJn%78BQ!Wf=h#v1HaLL47=7K@b5IAVcNg`3>v zB0odZdR}b84^gOa;5i3=oYH<>e{puIc6a(E7VyezNNR}GF2x;q`i#HDzFW)rZuh~AgN zJ-dCGT``4)FP_sn^gOd)yepXvh+asgcoO`W+GzIo{qx`666A|)*QFVLeC0E<7mik=GVh>Ie#pEG#4h7nt^=jdCbW4 ze&b2rDP-6iSnpv&yT{%<6)*aEvF*Jh1Z~k?>~69{bJ~{oL6^%$JTq?1Z#EO(g^rlJ zK&(84igZ?Xs_w7OpZ>h)SN6=(-^D}rcC^Rd5FWlH2Xk-&UjYYpQUd+KOi)FL-3rl9 z3C=NI?&sthrOv{^I+5zuD&YCRNW9pZj3;Bo-mg_AiX3zKxNLh)f1Th`R?UUHZa%pD zik-j2Q0XyF_6-VoK}K!xkY;nDC*_@W*{`BM(0{myz;twDSl%fq4U$~(x*9kO+7;Od z@fq53a{Ym)$PJDTKC;@cQgz~Uew)82jPX6-6we}%yfnNEDY%)l>VjjmKehUHu7dpc zgfw?N{W?#bX%r8(#5&if1W{f&CA?T%SiNXw>A2SIoa8#WtzEB^*@MMS8LBF_e4r3)BgwOfQxYO55Ls$&V#;atQe@q z+Uu8=bB0e!4MtNo^Yt)Rxfn&3u-S)Md|$HJlJfkJfNwszTa{$sX+kl-G&={Yp6+&?IW(s? z-dN1KN`BRa23nk0wN1E4Pt`l+0dnX2U;}-n&8-(C$KxK`EHs@WgPzsu9Hq(&QLPE*w?dvpFZwe>fJFb- z!6jv1gs6q;8_a5E>WF8y2JPSS{I8fDJKile)`-0Nm9-3KebJ}UDM%eHnu)pab?`fq z=Ev!(n~gkum*6zocQ$LHLRXx5%F$;9Is7>W1ZUPfHHIg25@$c+M1qA{dsLG-s)T$Z znklN`4pX>}6k=lAf69ESs;USG=JLHAS_QZg13(6mmwQzRP*<}6VJtB3-qj5Z{;#My z|BeVDSXo&E&KCyra^=%f&pJ#tM!w(6`Je%)r2)W!yO+Ad4`9^3i$GkF=VVNEj#pgl z6*D|jc;TA+_`(W!ybe)R&!QHDLmhQ5>`xl{b^{!8_8m(n1@pmiJH9fhZi3mw6(OaFY-fK>28Ysfn zZu;2=IrbzwbtiO_2*;TuYQqa32gP%nUMGmDS@`f;J?;q!xi%*Z10UQ*(ViES%^oH` zyG;~B3(Ikurcp;CkJD@1JJ3iT zGtHr)^4pHpZ}AFjdo#86+<*JYXZ(rsoptV8sMItv%h}dCIU_jiEci0Sco+8c1PI69 zmU$^>E?W~RXE_MQt>V5QHecGsY0r#~b9nDSp0^Z~g)(i6qetARaZ1Q$rVf7Lix+#B z0bCMJt1*_nz9i;<`5nSygSJM&(?3ew<$?Rs$;iI7ygJ;h6gXY6;eBl3A~=7oUQL2` zvGuuYD6URw?_=dCD;8i}KToRGwFcq~{RFAt7q^v{25n_rhwbRp$qz697x{kh8$<^7 zfwh6-ssBZ%I$vh zz{22hwi|gbR6v~uq|a*UDb{!tmIW~H9bC<~KrEU@rHml--Weva`M<1x|6hO}!cDL{ zWbTD?N-MRw!hE>N@>w$Q@M{!MU$30MO+a)k8igERBOVCdynO7teblIG6a;+9Gsurg ziWlppx!$pQn~9wa?8NboItXvLyS&0ynxbJ>3=pAV4rIvsxs=+{%6`Q8eS|~<@Z{SM zf7|<|K0*DcP#9O2E;9fiioR8bL9EgrwmY_%84we}Be@r6; zMxpM_pOl6F_8ptWhKTAB$?lj0Qz-!z$p*>Q)eDZydK|pJx(__rT>lg`Xse8#R94bi zJpMN)E}=SVweBe_NtdmF+E)JkUEfv~NR1%ditEE9o^1MeVAALQ2^?(u za4c-2`9w8*0k3=bg43_nM@*udR-eY5KeeM)rhs^tKa!S~-Jk!C<**={xwMFPQGrT* zx)Bg4FS!I$2Z;QY4*~Tsx&!@eFCg2?Wj#qMNW)I~TDQ^vL>2YLgu;@hna|#!WjiN{ z{q(zmhN1%803UmyfL-$hXQ2eKPV~+hNgEI_pzZj7N1jLT0{CoavK3O=^1MUwS-g+LEyn5g zwSf!`Fiktw7%+Y4Y;F^`MQPlq?y-+ce!JZ#;a?%C)PvBf+lV7N?86RIX7=w<<|RIE z8z7313S7O%`bbu3DIkiwR|O5s5ESleOmW#^)gU=m7kpP2ExMLH*-- z!Fg9Rg!gYgaLCXd2i`Xkw+Dor+(_7{xfzW=bX_qkJ0>rW0fwsMHNocndmQ|w|ILF7 z|4>o^G@pO^iG}|j8R$RhDFD(TNEcB4`W8~L0?h7iZ~nZRZ54@TOnvj$iKU1RAYK=; zQo5{sx@KLM-viu}J0N#ghz&g3@!xPIqC`wgXaKo=$A@m}J~MOl3$wKkFbQTA17Y8! z<123L+#|18EQbC#Q=P8^l=L!(Wr?%+M^^w}jovs1-g&ZczM5n>t7sycg+)yC3Gn|g zFHd*E0aJoFTJV&_=W^o>#On4Ec;(7Iuy12#Mb8JnD_te5V9{V~V?bQD7|sslmsnXZ z|7yNSq?UCBBku;@+&~Tp{FJKTm6e{dKj@qh9VjswK9`nF$0M@qzpLrzz0c;BpR>#+ zX%y?!XJXa@q&VB8JFITM18a_z=8p2-JiXn!yTt)wj+8H9bbpGrNfUs~&AI1J%gZ}W z$;X$*ZQqk*=o6YD=(#keswnQ}R_Bg9ueP4dR?d+CwcL9KfTYcqE0^M5?}S_eJP*l0 zOu~K+4PV|JK=tndNO&ZpZp~MNW)BdVHDR>SHN^drU?9PS)CQ6zmFE4uQ-7UX?*MYd#>9jd$ZuSk; zo4rOxgQlrjxBWWbBRKckRJ!YLOOTF+TR_Lnl81Xn9mQEd#Y`y>*-6=o;S`=XJ3KdZ z9u@~_?+G069ZX09q!%WNBlvN^9Vm$4bwH)&bX%xi2ogFX0Yranm(8(uVnZ+E60?$p ztKHdKj5koDwN>6_R`bYIqz#bj$yYB-oEFZACS?aL-WNsd!#M|lyOMG+UZ~047qnCj zw|-M;YvzjGyj*3!AOtu~fG6Vf9&iVu-&WNd3~cH5%{$JQ8{z-qAVjqVxVG7V^=CPQ zJUQ#YVqT0_6k62|S> z3r*Y4fS?zw$?X=nXVz=Iz12EJTC7LpYxXgdn?P2@Bo|5O!|VgOm+4jNte6BoT_fNr z?6B(?f!aA&a)5=}2DuNp$Ib$Sdu=0nqfw{AFUZLw)w4j8iLy&=YH*#=JW+#4FM7|*r%fEq~v!s+Fxv6 z>qg)RUk}MGmm?ikZV!2Y9mEb6y7snSm zYf#od;79Di@V4%nv3Ax3Qw|4f8!*lmfwxky!Tk1~DiASg8@pAYc>yu1v{hh`1p_lL z<)Gcs(RN>;SiAD|4w-(77jGCbvmNH8QPT^fWG92A8f*%lPlWUu$%9qKooM(BRCdv@ zG~{PLKEP^$VD?|E;iSQmu3#vElS#qIl!vg5bbbgIqHp<2N`K9lao&|n_v`a!omxBF z)J!%y8z+C*azS1O+tz&ykhhd-9{hz-cYFc%#|U2p=Trjv=Vx^0o=hoxOYK zh^+rdYtNPKA%yCBrXOQqaA+;OGhx*Fi}4j;z#@R*rO?=OsG5aI{Q3a!`R#U1sJ!+? zfaiD~W3jp=ry7V%SD4A_c^!101Iem>@pk_PAe#ZwZ{pEr3H~3womW^>TfgpAniz_R zG-)DA5v2FTU`LnKrP8EImo6ni0xC#Rlqe`IKtvRDNpFHgAO?(-Bq}ZtfdHXPNdicM zkg!MA_g$RxoU<>!i@opi%siQy88dUt@qhon_njWH6>?|Kohxt^2L30`O~fDTH|j(F z)jJH=iKa?0uT6}qvHm9fVpd`LhkU!{Jx;cS3>ipTbhi3RYlwK!4e4shgHPo=kRxMW zIK!#h3B<%3(2gpbIVEByouvH?sSsyMawR-Jo6#3d#UsZu`l|uGR~R$kQg(1P=RNbI zJPARADfoXD91MvoHnhleE4EzYp<3Fv0O9yNa`GdBq2hXkgnD%12+LtwjsNx6Z(1FRR`3qB!zk&z_z3} zp1x`TbN(Dh`~}bx!<4@e$#~8*{Z2o|IM_c^ICv|+JMgNTljH1X$Ypt?Pe*7JXNy$F zn6f$4zMy<->yCU_v&xd2o(610Ncz3jvqHHU!CrrZ|oV{%|f@^ zQOBp>o*JC}6NK2nHW%x`+>*92!4ykHC~J${6UZBUG2Q~Gj=rug$U_+cx{34BYzE?~ zrl#g{;ZB$e{#1Fu5z}4+4d+nSdWz-lJsxVv1YG}g%7+7xTPL*d;>r#V%poJ0wiyf8 zvUg7VjtzC}{4es1V(*U{sc2IR?pAQ$y9*Vi$+)cm+%nh|##23r<^EL-MQ zx;FkLH~S9-d9X&4kUbPRwW4jEl*=mknT{xf4T6?`bIZo~!`+yRF6VtyY+Orh&bY}C zg?hU;rrY!n%O33veqhOc0O;@07()f*h)Z7WLBF~9!AkWvlc#X0xTp`oTF7nZ_&ra* zaO3NKIQq}(shb*_OQAgCU~vg6uG8xq?l^hPJyaR7*(pSEQAL|&_4IhCe@Z(EFi7Uc zxoHyen>&DUxQ{)uj8=aWY~1|v5OO;S?U$$-4RD`8@JBGVdk}{v3z~}irXm_WV)E%8 zPgjxeR>@FOB7Zxw1Z=g(@%fzWRF;J(Ut1M|AfFp!@Q-5$}^;l9&s!b+AluVHw+K!B{O_WqGAtdfs=`_6f@A5gne@+fO-bTY!B1j6;^(*A{&}X0_-?Z!$pW5LBO0 z&2@#|#sVj>V&>^wWdy_KpQP*N!(;D{YSA@LVUDu%RDy*o{$^Np%qZwsS;e!w_%<&l z4G(Iz`U^OwxcGQ)3-S%D0tuzyr~WWr({${OZ+BLi-4Qmix2zJ-g!x>)R5l#>;3$vuh4z(;U#XB~`AQR)$$?aOzd?pnAt?zHU z?0qLlNpP)25z)}xnb;)gcoZQx2V)lbT=Tm@YwtADM6S-KfaEy!t5QG0?3g;z78xqU zI_`@&rdU})R#C3$em&w=8nQ>#Br$=UvcwP}19k5lS0~RMDhGp9Bb+Xy2Mu(QU74yI@&WML zOlI--oW3T{>d^u_2vbwb(Z+=|z0zN9XDTC9p0YkPB}KO)DwjBEtak4kFu~oPKzO$c zLkN9(Fqhr6JN#6pyzh{A-II~?L|xM;ckY@oz{?(9?3h)Q@YHT-7QLn{M9&(!_bF%# z9+GrTc^~uj>DZr!pW`x}BkKISCMm0(e&(6IH;S2)L`-7cgWrcwMaC7M9=O~BqbO6_ z(>T^RIL@c%kRZ&p}2xvBJ(`t-ITEuif`WoPCC^_xjp#Y zF>wXr^ahQvOC-pIQg^P0-lHyO{%|{!QH_>^1*C2++M-(&smsd&92}+ zvY})VRveRdR!wB%fN5BT9JUbkX^7ZjJG6g(q$0HA#RX5Z!Ctjl&LzW9LIPN4FZo~H zIzHXiSRez=1LOIGqztU1V!3`J8IOm?#vYW!sD77V{a&cXG&xFV0RW-h9Iu@>GW*Q``Mlrfevd&gMbcCvPg`Aa9P7*Z^`{*b9q zJ3e(Yz4t~Q!ow|Rm7#+z;+o^9H`nHUEdFYNL5jx7e@IWkq+w)O8BVM@%;Yfx%Fv&F z0D|a_ppRFp>#zVKSWvz<%C$M*Zo*|Sqsn9ZkR|)vB&S91I%RijDh00>G3PauTpSvj*d}O=bD+CIp21oF?Ic=sjgC?1mL_u$`3j z#-$!N-zXBIdxi;kCXc|Y^D8X+s^2$d43*1xAeETR)1O~`Ia_w?`b?j>Eqf`FJe)u_ z{*y8^M(I;s08anwSBq0cb&X8>N2Cdwx>bMU#(7qe)r^&0UfGvCdM=z)CkjEAh8kiU zIBF?7lc&K+Gv!2Yd_gfh&C23ZZUub!VDyGdoV;h+EFvc!enU1^t|%DJl)66u^{I2q zifKdGXrFNLiJH25A-~sJUb+4)SwlqZJ$lUiAht>=wJSRnfCX=mvdgC>9>X_jW5=Iq?ED8{eLd%!QMp& zVWqXBKL|?%4mbIw;{TM@bn6^*aZ&*OjpJq5D?ZnYdxHZnFj={)s&RxwP=++2>g9O2ox zARu8BBsiRKcBT%h(rC2t)s4O;fZFD^8_Njxaygq)Jgd%azhk{_Iqx@)c@k2LrNPHM z0oq6!jBo(VLd*-$)&4wji>MSY0@%Q8 z=&$oQS;*BpoyrpJvvIRUvzeL-AXzeyEIjrS)KWcK63WVGa0hSknF^HZDYytMRsNLX zr9=yDO-*4aVj1v3VsA%oKs*wDUE#6LuN*566vr={Zvm8h`P=P*%Bz?-+(!CI9vW^4 z15nf|If!QodKbHNANYApdv9x6JL^$Exu~G^7#ICfF5np`RrA1&9eOS^hjJmv5(1z+ z;y;M%Ise9oBjZIVCasI)t_`7i0Xf2iXS3m9e$Y59C18)2bOUGs8WNr!y<&i11IW{$ zmr+#*Vz=@vV*YBm`r5hnkP>qIPQyGP8qqv#CGA=THFPwwV^OP}z^n0dCSng51>Oeec1sk0 zrYeg^5T4;FDv>;-og_{m_~17{5xkTsJZ_S#Uz2`vty>+N=M(Yuf_naK3lN7(YOo(t zyi_bt<#1Lhmj3QQtN93Ijs8p*|9(TL?UoLxjg4lz32JrVcp8>gxC2rz)Sl|KlEaxs(Cve@$g7=bVg2p~gh zI6UXCYUkc`19iXufp#bVL7nNfALIJ8o5=cZzqjFI#Z8uRM?k;K?Ggw=o+;1*9$lVM zYrp-Lew_0o9oL54qypn8B0q!JS>wI454!blKicxQ9o)d*EIJyrp)sys?g~qeXu3=a z%j;je+j86GJZrw6xW#r(ws7U?aVckQlk+A%)Jzv0*EOJo0wv^3&0bb^in*sp4a023 zs`pgR0K%Jr~)` z0fNx+O3f!rGq_JKt?{&b>I_~C<|@rwwjpHbtM+iMj}>Z*NetTTh@oIi#nG^pbaUyg zFHs5jMD~Ob&*8QR#i$#$3Oei6?^SP$7x)D(#DEz&vxsLyysr5qkUdTMkz$_lOUB>c zI8m&9CVDueJ&@<`VipQEc$KwvF>ELVn<9)ReSLXx)SPd1g$y%tM!v6&7>}GWpC*Z- z2_r30r0CQoo)FneZyUCrMU5!kN>h?981c-UUE?hzL(qf56u#4vrmEM9WiT$|EgX+DFPqXyNx4{C9@R_lhj9UU72wtCDG3ow?`z zhl_h_ON$B5K~=9rJ#Wu2e&h*6dp+g`A5#ZKD>_K`C0i_LBdmAOAIK^^e*9Ps4~%@I zvkDIog19`SP^arIy4a1c0a0Km3!qNi-RZ+RPE`ty70%vpWzl7I0oH5&N%zTeY~uA@ zDTvZ7TVRJWkP#2o)t;->+ucLIRj`J%pa%fr}Cjy-*$FXihl4o zb&Xfi6Xr|IwgE@|bu^~-#AJ)VN(c0tj{Cc3K&F}@c5b*lW2O;{rh*RwB(xe>QZMS{>q{TrQYyr5Mgn;ggAt{1ql#drIMvx$fEs|(GaYyZ zBnx2W!1Y)#B*Yy^FX;%47%e1kAjOiZC6?7$xtm}9x}tSSwvMqzHmn}}EH50E+;3;e ziH`CxLnFeP=`fba{+{KxI3Ptv^>+r1*ipThZVIA_rqy0e_f*KM3x9t^wz02HjJ6?` zsHj!a%x0Pg@01}$kjLu$QZip!Tet#a4pO34&)cS}l>la7y3J0x`y@?Xt&7MH*>@A@ z%YDLoK4uupVq|Y5spkpof_`oZYhzE6=2y(S%A~?MTanCrngf5}I8{Jl1DVn;Yja=t zy1To}{5y^!8SZ|LY(*qX;Xz7V#GM1$Z+v=^ZsUnb{RGHpHn`Go{iWlll2TML5zei$ zmWj9)CFe=i>VveR8bqJ7p)g!s`%E&E9p_E<-y@v|BfI6;nC{2H+S_LXt9DW=qp-M* zmg-Kw=56itWb}nOZt9*QkH+g)qO8+RMMjhbAsD#a3p|lZnC#mHcP>5m@M{uwvu_tv zcJLRdncrc*($h=}N88d8DHSu6<+pvow@sZ0siKCBRltq#GWaO@A#lk<3L< z**(X&RkF``O308Cb^nT);dxweZlhZ`2R|4VRxJwOqNV5tpV+9hPlYh~nafGW(o|6< z5#Ql0AFk(kGVV|3QGeqnh=wMHZ8AweWS{qy!Ufhc5}hNH7s))1RXTEC*nkqL}F8biA0U)Fu^YgE#v7Ck2sgqu7x@1 ztOs^6jq|VUQ&=+Y;MR?ECbgPl{UOPBRXSQds84*q9gc0Ndv|W-6u7*rg$69*>O185 z1>qF5O~?-scz{*%#3zhQad_&H%~;&n@oM*VbAfQxH^HV_6qd&b6)h{RE2sz#o}@ce6h(c6^d>_ii|y#dyep!2aly9{B=h%z-bJ7~ zjv@Y#y9I^G6sD;V=$$idrTBtKhJIgVA6f2JHZE~PKER$SY&Y=ed-a@qE1ndIAJcz3 zC`ia?S2iiFpEqg;%*2Fvij5_xhX=AGJkoJOlWtY<2_tL*M!|2zKyP4`< zcMNSwH2G4qF&@Ye&iztqg`t0d))Z3RS7dA6=MurZ~3*Jw$DaQ01 zGs|IO96r7%ajVo|S%?^L7+ge<0!szz<4DeyzBR^#RCE*hdz;H@7b#DpZQ(GGHa+jou5LWRD_qDIA(s^SLJy z^aEbA;pYZBfP{UWD(6KV)vUe}HVBbjhCoiQhcgP9s2kW8`c@b$*Toyk0X-g{8F3E0 zvrIqrAY}MKL1+4)1mq|@sTZgoeh>RLTeX@vQSu@OZLtP^_ca#{Tfv{taZr(uQ;LQg zY@UayqayPwYP!wY_u%4`gusOpwtiRqYqm2L+0>TVXu&-!!`5I{V3jC0jxXHg`oc#DYgrti)U zWyM=4R9{x_s{SXQiwAUNOnU429xO~n>rf=g<+f}engBkVaw@6+{kmOK#>zDsIyVLM z;$79E-AkH7m$k-P&?CxC^($gsl3NLeAN-t=HT`IkmzxYdiCu*A*>`{Jsj-suPwmUd zoppPs&K@61_&WDdKHQph*RiHAQxjq{P~e;~g)X#AOx>>=BHauVd-C_%gG|Zf6z{nu zajUy68RhojK5tHsiJtJ%~nMJ)TVQ9OU zT86JIbEgp1WyLnnSe%aXMTpkudCjdh*FEz;_F-X+wn2;OI`y0<)|Y+S7^)!5nClQ) zonuFCU=9C@VQeBfaFOUlEYRZB+_zO(DT8v9tB!*b(EK`QydOm7ts&lY55L^b*p8U49nU_$C9h;s>HBSp?fFs71}?aKJpE*d|8)Cj zyc*oxzzG-~1n@w{$$W4T+$s9g_z9HkM4w#g|vy`jmQw^0eOV1>;qY`0Lee>q& zwknhGEwvgP>waOg%<-&0M_xzoMq3M8x5B%X8+3^4e6PZDhSS7H9jroFSk9~YD(G5Q zT&56DtMN~?yq%Hzg7!H`-7C^sxX+xJMo%3c_Tc>MC_kERa}FVAqF(*G5ld0y>$kwj(` z>NPUrcjAN5!ld`Rtp+pxpiSPCSXQu7m4a}syJE`^!dDL?o&@*a^A42HxsT5NlaJ(% z`I9lfQ~76(K{iC{pV2rfO*EB}I^BO-v-#IBN5j-x_Z|0!#w*GZnf0=RKh}~L2}c(0 z3^b}nTopzX1l#=~8wL|#$NCsFL9qeVc3s%%50UYoP$_o+8F&~{Ug)5ZJzSK&w3IQ~ zc)w%nrO;>ZE{C<2GpVY~cao8FKleGi4X4p_6wfTGfnkmPn-KY9eLt?zP_EQu;|uN& z{+@UJdn9?srKUz6&N%9+*l{Dk%%D#8ru)rZ%bN4wG`K6p9V;b0!W~toPf`#|YsvWz z<_6g`<3kFd8Cz<;b#g)YPjdw)8J5pT(rGr#SY+_56#1Gf5Ztov39<(OIO z3rsv!)q7biqXTaUc2!Wd6M+8#yIn6SHs1Z-8_9T1leH%ob=B(%EEUFFM$F99E{Ez= zxhRAePS`>sxkD4LCUI|%AKzu$^~!!lRMFzwzZ0{70M-iN<1;swffQhXK9rV{G6B}s zPP~+!#s7ae>i>fpx#w}Of@9`Aa^_!YscLSQ;{o~P6VrL?UY6>9y?-KEqt5)}Wpj6J z=0#H+veGg-{cRS!aQsIYU3tPKgRiUr&&qLce(~Z(Gmsk(nM)Qb22EV!j^VeBra?n+ z#E!R-u;`V_f6*z*rN-;dbuhDJ`SgkSr_H7pKte~FC9tR!VUv*W&mMsTu+~@2lLBvA zX^aI|zn`(Qu@UeDbPW#Y9S-x{Z$ZM0#RKU{6Y0u;mDCle9Xd1w3A?vv+GaAIfx?qq zs7CazAUwU(n=S<-$RGPki(Y)K9C+4`%~(?bETzxfAj4BA6Z?Ui|Hs0z2;7vZ%+Y zs1mwdcZjQFFMsbm3OI?2oB{*X>o@9LsTk?s%gkL}5MjDu+Qv$f8uBKim1QttfMQ3K zhrkuosCoiI=NW6`L3petvQV8JHP`UK*Tl{?M@mBTzH?x};d&-MX;!!hi+vwi;+^$n ocKn12z1}pF&Kijl=PD;Zmp?awwR8thsoHVjtnHbqQ(j5`1-&f=Qvd(} literal 27833 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 From ad1f7578188a0156f031a1aceb25baedc0dd932b Mon Sep 17 00:00:00 2001 From: sibowler Date: Wed, 20 Jul 2022 06:14:12 +1000 Subject: [PATCH 09/26] Implementing RESET Tuya function to enable devices which are stuck in a zombie mode to start responding to commands. Added configurable DPS for Reset command as found devices wouldn't 'wake up' unless they had exactly the right entries --- README.md | 2 + custom_components/localtuya/common.py | 80 +++++++++++++----- custom_components/localtuya/config_flow.py | 35 ++++++-- custom_components/localtuya/const.py | 1 + .../localtuya/pytuya/__init__.py | 55 +++++++++--- .../localtuya/translations/en.json | 3 +- img/2-device.png | Bin 27806 -> 29877 bytes 7 files changed, 140 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 756347f..edb7fbd 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ Setting the scan interval is optional, it is only needed if energy/power values 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. ![image](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 38401bc..5eb6d81 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -35,6 +35,7 @@ from .const import ( CONF_DEFAULT_VALUE, ATTR_STATE, CONF_RESTORE_ON_RECONNECT, + CONF_RESET_DPIDS, ) _LOGGER = logging.getLogger(__name__) @@ -143,6 +144,14 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 @@ -181,14 +190,60 @@ 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 - self.debug("Retrieving initial state") - status = await self._interface.status() - if status is None: - raise Exception("Failed to retrieve status") + 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.status_updated(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: @@ -216,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): diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 203c0c6..e70076f 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -38,6 +38,7 @@ from .const import ( CONF_NO_CLOUD, CONF_PRODUCT_NAME, CONF_PROTOCOL_VERSION, + CONF_RESET_DPIDS, CONF_SETUP_CLOUD, CONF_USER_ID, DATA_CLOUD, @@ -90,6 +91,7 @@ CONFIGURE_DEVICE_SCHEMA = vol.Schema( 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, } ) @@ -102,6 +104,7 @@ DEVICE_SCHEMA = vol.Schema( 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, } ) @@ -144,6 +147,7 @@ def options_schema(entities): 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), @@ -235,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], @@ -243,20 +249,39 @@ async def validate_input(hass: core.HomeAssistant, data): float(data[CONF_PROTOCOL_VERSION]), ) - detected_dps = await interface.detect_available_dps() + 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 = data[CONF_MANUAL_DPS].split(",") + 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: - # trim off any whitespace - new_dps = new_dps.strip() + 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 diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 993a1f1..9ae3902 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -43,6 +43,7 @@ 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" diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index b7645ec..d36cd5e 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -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,9 +313,19 @@ 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: - self.debug("Got status update") - self.listener(msg) + 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: self.debug( "Got message type %d for unknown listener %d: %s", @@ -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. diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index a97f120..d1da446 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -97,7 +97,8 @@ "protocol_version": "Protocol Version", "scan_interval": "Scan interval (seconds, only when not updating automatically)", "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)" + "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": { diff --git a/img/2-device.png b/img/2-device.png index 335d787033860e2b249bf04698cdce77d8afba8c..4e6623d11dbaa187d29a9afa11e4b63f4442c312 100644 GIT binary patch literal 29877 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 B9gP40 literal 27806 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 From 8a696cbaf2e63d44eca03e3ab9a35a045d115213 Mon Sep 17 00:00:00 2001 From: Fabien Brachere Date: Tue, 28 Jun 2022 08:50:25 +0200 Subject: [PATCH 10/26] Add "1/0" entry for HVAC_MODE_SETS BHT-002 thermostat DP #4 set the heating mode (auto or manual). The accepted values are strings "0" for auto mode and "1" for manual mode. This commit add an entry "1/0" to support this behaviour. --- custom_components/localtuya/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 1400ec7..f6433b2 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -72,6 +72,10 @@ HVAC_MODE_SETS = { "True/False": { HVAC_MODE_HEAT: True, }, + "1/0": { + HVAC_MODE_HEAT: "1", + HVAC_MODE_AUTO: "0", + }, } HVAC_ACTION_SETS = { "True/False": { From fdc9862a3398ab2dade00c1cec10bb5ddeb40f64 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:20:41 +0200 Subject: [PATCH 11/26] Update requirements_test.txt --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8cfbf7c..bc7bfbf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,4 +5,4 @@ mypy==0.901 pydocstyle==6.1.1 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.12.10 +homeassistant==2022.8.1 From 205a8445eec3a8295b604b2fe6e87967b23f71e4 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:49:33 +0200 Subject: [PATCH 12/26] Reverting requirements.txt --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bc7bfbf..8cfbf7c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,4 +5,4 @@ mypy==0.901 pydocstyle==6.1.1 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2022.8.1 +homeassistant==2021.12.10 From 43c01eefd93215d4a8cb035dbcb927ecee7c52e3 Mon Sep 17 00:00:00 2001 From: Mark Breen Date: Fri, 8 Jul 2022 00:27:08 +0100 Subject: [PATCH 13/26] Refactor number entity overrides Relates to issues [929](https://github.com/rospogrigio/localtuya/issues/929), [939](https://github.com/rospogrigio/localtuya/issues/939) & [941](https://github.com/rospogrigio/localtuya/issues/941) I do not have any suitable tuya devices in use to test against this number entity however the warning in logs "LocaltuyaNumber is overriding deprecated methods on an instance of NumberEntity] is now gone https://developers.home-assistant.io/blog/2022/06/14/number_entity_refactoring/ --- custom_components/localtuya/number.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 596eb01..ba7c3ba 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -10,8 +10,8 @@ from .common import LocalTuyaEntity, async_setup_entry _LOGGER = logging.getLogger(__name__) -CONF_MIN_VALUE = "min_value" -CONF_MAX_VALUE = "max_value" +CONF_MIN_VALUE = "native_min_value" +CONF_MAX_VALUE = "native_max_value" DEFAULT_MIN = 0 DEFAULT_MAX = 100000 @@ -52,17 +52,17 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): self._max_value = self._config.get(CONF_MAX_VALUE) @property - def value(self) -> float: + def native_value(self) -> float: """Return sensor state.""" return self._state @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return self._min_value @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" return self._max_value @@ -71,7 +71,7 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): """Return the class of this device.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await self._device.set_dp(value, self._dp_id) From 2bae03c679a24525def2bdb0a2999530aefa68e4 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:28:08 +0200 Subject: [PATCH 14/26] Release 4.0.2 --- custom_components/localtuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index bfaf4cc..67c90f4 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "4.0.1", + "version": "4.0.2", "documentation": "https://github.com/rospogrigio/localtuya/", "dependencies": [], "codeowners": [ From 8de987b2a0d4d090cfe9868bb5e3aa5742d2e360 Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 1 Sep 2022 22:25:24 +1000 Subject: [PATCH 15/26] Fix for cover defect --- custom_components/localtuya/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index b9c10f7..060dbb6 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -190,7 +190,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): def status_updated(self): """Device status was updated.""" - super.status_updated(self) + super().status_updated() self._previous_state = self._state self._state = self.dps(self._dp_id) From 8c4894371f245d90242cc329212c98d30ffd41b9 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sun, 4 Sep 2022 14:02:06 +1000 Subject: [PATCH 16/26] Fix for cover issues with new state restore functionality --- custom_components/localtuya/cover.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index 060dbb6..fa7a4fd 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -190,7 +190,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): def status_updated(self): """Device status was updated.""" - super().status_updated() self._previous_state = self._state self._state = self.dps(self._dp_id) @@ -230,5 +229,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) From 19a0cb6d15009e32b49fa780b3cc5d2882a1c875 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sun, 4 Sep 2022 14:06:10 +1000 Subject: [PATCH 17/26] Fixing slight format error --- custom_components/localtuya/cover.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index fa7a4fd..3b6b86d 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -190,7 +190,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): def status_updated(self): """Device status was updated.""" - self._previous_state = self._state self._state = self.dps(self._dp_id) if self._state.isupper(): From 21e6aade35de1f9d15d1a767bf78b8debfc69885 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Mon, 5 Sep 2022 10:16:11 +0200 Subject: [PATCH 18/26] Edited manifest.json to 4.1.0 --- custom_components/localtuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index 67c90f4..d4f55f6 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -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": [ From fa55cd6a38d68ec608e7245f5f8d4021a0cadabb Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 5 Sep 2022 10:21:24 +0200 Subject: [PATCH 19/26] Ported README.md updates to info.md --- info.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/info.md b/info.md index 900b0e0..c346691 100644 --- a/info.md +++ b/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. -![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) - +![image](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) 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. From 46e55733259b3fc89136c6cde404a8f91c695a8b Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 5 Sep 2022 22:58:11 +0200 Subject: [PATCH 20/26] Removed trailing space --- custom_components/localtuya/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 80743df..6379f5f 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -49,7 +49,7 @@ def flow_schema(dps): 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, - vol.Optional(CONF_FAN_DPS_TYPE, default='str'): vol.In(["str", "int"]), + vol.Optional(CONF_FAN_DPS_TYPE, default='str'): vol.In(["str", "int"]), } From 21f7af6ba8c6c58738d946f793219fb41e153dee Mon Sep 17 00:00:00 2001 From: sibowler Date: Wed, 7 Sep 2022 23:21:06 +1000 Subject: [PATCH 21/26] Implementing safe default for devices not requiring RESET --- custom_components/localtuya/common.py | 33 ++++++++++++---------- custom_components/localtuya/config_flow.py | 28 +++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 5eb6d81..9cefe81 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -208,20 +208,23 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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) + if (self._default_reset_dpids is not None) and ( + len(self._default_reset_dpids) > 0 + ): + 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.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) + self._interface.start_heartbeat() + self.status_updated(status) except UnicodeDecodeError as e: # pylint: disable=broad-except self.exception( @@ -552,10 +555,10 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): 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): + 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", + "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, ) diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index e70076f..1eeb3b5 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -248,24 +248,24 @@ async def validate_input(hass: core.HomeAssistant, data): data[CONF_LOCAL_KEY], float(data[CONF_PROTOCOL_VERSION]), ) - + 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, + ) 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() + if len(reset_ids) > 0: + 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 = {} @@ -282,7 +282,7 @@ async def validate_input(hass: core.HomeAssistant, data): 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: + if str(new_dps) not in detected_dps: detected_dps[new_dps] = -1 except (ConnectionRefusedError, ConnectionResetError) as ex: From 1db48e5012ccf9b27d6ec3778ea6ed0f256c1128 Mon Sep 17 00:00:00 2001 From: sibowler Date: Thu, 8 Sep 2022 06:12:19 +1000 Subject: [PATCH 22/26] Updating readme information to reflect the change. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edb7fbd..3b5069d 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Setting the scan interval is optional, it is only needed if energy/power values 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. +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). This scenario mostly occurs when the device is blocked from accessing the internet. The DPids will vary between devices, but typically "18,19,20" is used. 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. From f7ce9be5f6b2df2df26133e8c37c0918c0b6ec6c Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 10 Sep 2022 06:40:53 +1000 Subject: [PATCH 23/26] Changes to make Passive DPS entites a configurable item --- custom_components/localtuya/common.py | 19 +++++++++++++++---- custom_components/localtuya/const.py | 1 + custom_components/localtuya/number.py | 4 +++- custom_components/localtuya/select.py | 4 +++- custom_components/localtuya/switch.py | 6 ++++-- .../localtuya/translations/en.json | 3 ++- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 9cefe81..aa8992f 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -36,6 +36,7 @@ from .const import ( ATTR_STATE, CONF_RESTORE_ON_RECONNECT, CONF_RESET_DPIDS, + CONF_PASSIVE_ENTITY, ) _LOGGER = logging.getLogger(__name__) @@ -375,6 +376,9 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): # Default value is available to be provided by Platform entities if required self._default_value = self._config.get(CONF_DEFAULT_VALUE) + # Determine whether is a passive entity + self._is_passive_entity = self._config.get(CONF_PASSIVE_ENTITY) or False + """ Restore on connect setting is available to be provided by Platform entities if required""" self._restore_on_reconnect = ( @@ -555,10 +559,13 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): 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): + if (not self.restore_on_reconnect) and ( + (str(self._dp_id) in self._status) or (not self._is_passive_entity) + ): self.debug( "Entity %s (DP %d) - Not restoring as restore on reconnect is " - + "disabled for this entity and the entity has an initial status", + + "disabled for this entity and the entity has an initial status " + + "or it is not a passive entity", self.name, self._dp_id, ) @@ -575,8 +582,12 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): # 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() + if self._is_passive_entity: + self.debug("No last restored state - using default") + restore_state = self.default_value() + else: + self.debug("Not a passive entity and no state found - aborting restore") + return self.debug( "Entity %s (DP %d) - Restoring state: %s", diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9ae3902..a8d905a 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -44,6 +44,7 @@ CONF_NO_CLOUD = "no_cloud" CONF_MANUAL_DPS = "manual_dps_strings" CONF_DEFAULT_VALUE = "dps_default_value" CONF_RESET_DPIDS = "reset_dpids" +CONF_PASSIVE_ENTITY = "is_passive_entity" # light CONF_BRIGHTNESS_LOWER = "brightness_lower" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index e8ab53d..23d7ea9 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -14,6 +14,7 @@ from .const import ( CONF_DEFAULT_VALUE, CONF_RESTORE_ON_RECONNECT, CONF_STEPSIZE_VALUE, + CONF_PASSIVE_ENTITY, ) _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,9 @@ def flow_schema(dps): vol.Coerce(float), vol.Range(min=0.0, max=1000000.0), ), - vol.Optional(CONF_DEFAULT_VALUE): str, vol.Required(CONF_RESTORE_ON_RECONNECT): bool, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, } diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 75dd217..f643e08 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -16,6 +16,7 @@ from .const import ( CONF_OPTIONS_FRIENDLY, CONF_DEFAULT_VALUE, CONF_RESTORE_ON_RECONNECT, + CONF_PASSIVE_ENTITY, ) @@ -24,8 +25,9 @@ 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, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, } diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index 40e8ea1..bc664bf 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -16,6 +16,7 @@ from .const import ( CONF_VOLTAGE, CONF_DEFAULT_VALUE, CONF_RESTORE_ON_RECONNECT, + CONF_PASSIVE_ENTITY, ) _LOGGER = logging.getLogger(__name__) @@ -27,8 +28,9 @@ 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, + vol.Required(CONF_PASSIVE_ENTITY): bool, + vol.Optional(CONF_DEFAULT_VALUE): str, } @@ -82,7 +84,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): # Default value is the "OFF" state def entity_default_value(self): - """Return False as the defaualt value for this entity type.""" + """Return False as the default value for this entity type.""" return False diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index d1da446..5d65073 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -188,7 +188,8 @@ "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" + "step_size": "Minimum increment between numbers", + "is_passive_entity": "Passive entity - requires integration to send initialisation value" } } } From 28cbfdb2bd7802ed17362ac449f01667b49343f9 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 10 Sep 2022 06:57:38 +1000 Subject: [PATCH 24/26] Updating documentation --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b5069d..4698afc 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ 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 '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. **Note: Any DPS added using this option will have a -1 value during setup.** 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). This scenario mostly occurs when the device is blocked from accessing the internet. The DPids will vary between devices, but typically "18,19,20" is used. If the wrong entries are added here, then the device may not come out of the zombie state. Typically only sensor DPIDs entered here. @@ -107,8 +107,11 @@ After you have defined all the needed entities, leave the "Do not add more entit ![entity_type](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/3-entity_type.png) For each entity, the associated DP has to be selected. All the options requiring to select a DP will provide a drop-down menu showing -all the available DPs found on the device (with their current status!!) for easy identification. Each entity type has different options -to be configured. Here is an example for the "switch" entity: +all the available DPs found on the device (with their current status!!) for easy identification. + +**Note: If your device requires an LocalTuya to send an initialisation value to the entity for it to work, this can be configured (in supported entities) through the 'Passive entity' option. Optionally you can specify the initialisation value to be sent** + +Each entity type has different options to be configured. Here is an example for the "switch" entity: ![entity](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/4-entity.png) From 85a1ae069c864068760b909fcb5ddf4c07d9c0c3 Mon Sep 17 00:00:00 2001 From: DeeSe Date: Thu, 29 Sep 2022 14:05:07 +0200 Subject: [PATCH 25/26] Lint fix --- custom_components/localtuya/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 6379f5f..584ea84 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -32,7 +32,7 @@ from .const import ( CONF_FAN_SPEED_CONTROL, CONF_FAN_SPEED_MAX, CONF_FAN_SPEED_MIN, - CONF_FAN_DPS_TYPE + CONF_FAN_DPS_TYPE, ) _LOGGER = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def flow_schema(dps): 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, - vol.Optional(CONF_FAN_DPS_TYPE, default='str'): vol.In(["str", "int"]), + vol.Optional(CONF_FAN_DPS_TYPE, default="str"): vol.In(["str", "int"]), } From ee25098f63af3e64cdaf48bed0568d0b6ee0b408 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Tue, 11 Oct 2022 11:49:04 +0200 Subject: [PATCH 26/26] Update manifest.json --- custom_components/localtuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index d4f55f6..10b1b5a 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "4.1.0", + "version": "4.1.1", "documentation": "https://github.com/rospogrigio/localtuya/", "dependencies": [], "codeowners": [