From 9bfeb107f546774fd26115576c9ab088abb32fa8 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Wed, 30 Dec 2020 22:47:45 +0100 Subject: [PATCH] Cover fake position persistent (#213) * Introduced read/store to JSON file of cover's last position for fake positioning * renamed "fake" positioning to "timed"; introduced timed movement timeout equal to full opening time * Made waiting for timeout for stopping non-blocking * Introduced status storing using RestoreEntity * Fixed postlund's remarks * Updated cover samples in README and info.md --- README.md | 4 +- custom_components/localtuya/__init__.py | 4 +- custom_components/localtuya/common.py | 14 ++++- custom_components/localtuya/cover.py | 52 +++++++++++++++---- .../localtuya/translations/en.json | 12 ++--- info.md | 4 +- 6 files changed, 65 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 15dd1ce..9981bd3 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,10 @@ localtuya: friendly_name: Device Cover id: 2 open_close_cmds: ["on_off","open_close"] # Optional, default: "on_off" - positioning_mode: ["none","position","fake"] # Optional, default: "none" + positioning_mode: ["none","position","timed"] # Optional, default: "none" currpos_dps: 3 # Optional, required only for "position" mode setpos_dps: 4 # Optional, required only for "position" mode - span_time: 25 # Full movement time: Optional, required only for "fake" mode + span_time: 25 # Full movement time: Optional, required only for "timed" mode - platform: fan friendly_name: Device Fan diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index c915a84..5e142e9 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -22,11 +22,11 @@ localtuya: id: 2 commands_set: # Optional, default: "on_off_stop" ["on_off_stop","open_close_stop","fz_zz_stop","1_2_3"] - positioning_mode: ["none","position","fake"] # Optional, default: "none" + positioning_mode: ["none","position","timed"] # Optional, default: "none" currpos_dp: 3 # Optional, required only for "position" mode setpos_dp: 4 # Optional, required only for "position" mode position_inverted: [True,False] # Optional, default: False - span_time: 25 # Full movement time: Optional, required only for "fake" mode + span_time: 25 # Full movement time: Optional, required only for "timed" mode - platform: fan friendly_name: Device Fan diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index e158f69..809259d 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.restore_state import RestoreEntity from . import pytuya from .const import ( @@ -209,7 +209,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.debug("Disconnected - waiting for discovery broadcast") -class LocalTuyaEntity(Entity, pytuya.ContextualLogger): +class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): """Representation of a Tuya entity.""" def __init__(self, device, config_entry, dp_id, logger, **kwargs): @@ -228,6 +228,10 @@ class LocalTuyaEntity(Entity, pytuya.ContextualLogger): self.debug("Adding %s with configuration: %s", self.entity_id, self._config) + state = await self.async_get_last_state() + if state: + self.status_restored(state) + def _update_handler(status): """Update entity state when status was updated.""" if status is not None: @@ -314,3 +318,9 @@ class LocalTuyaEntity(Entity, pytuya.ContextualLogger): Override in subclasses and update entity specific state. """ + + def status_restored(self, stored_state): + """Device status was restored. + + Override in subclasses and update entity specific state. + """ diff --git a/custom_components/localtuya/cover.py b/custom_components/localtuya/cover.py index d8da253..e4a135a 100644 --- a/custom_components/localtuya/cover.py +++ b/custom_components/localtuya/cover.py @@ -33,7 +33,8 @@ COVER_FZZZ_CMDS = "fz_zz_stop" COVER_12_CMDS = "1_2_3" COVER_MODE_NONE = "none" COVER_MODE_POSITION = "position" -COVER_MODE_FAKE = "fake" +COVER_MODE_TIMED = "timed" +COVER_TIMEOUT_TOLERANCE = 3.0 DEFAULT_COMMANDS_SET = COVER_ONOFF_CMDS DEFAULT_POSITIONING_MODE = COVER_MODE_NONE @@ -47,7 +48,7 @@ def flow_schema(dps): [COVER_ONOFF_CMDS, COVER_OPENCLOSE_CMDS, COVER_FZZZ_CMDS, COVER_12_CMDS] ), vol.Optional(CONF_POSITIONING_MODE, default=DEFAULT_POSITIONING_MODE): vol.In( - [COVER_MODE_NONE, COVER_MODE_POSITION, COVER_MODE_FAKE] + [COVER_MODE_NONE, COVER_MODE_POSITION, COVER_MODE_TIMED] ), vol.Optional(CONF_CURRENT_POSITION_DP): vol.In(dps), vol.Optional(CONF_SET_POSITION_DP): vol.In(dps), @@ -64,8 +65,6 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): def __init__(self, device, config_entry, switchid, **kwargs): """Initialize a new LocaltuyaCover.""" super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) - self._state = None - self._current_cover_position = 50 commands_set = DEFAULT_COMMANDS_SET if self.has_config(CONF_COMMANDS_SET): commands_set = self._config[CONF_COMMANDS_SET] @@ -73,7 +72,9 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): self._close_cmd = commands_set.split("_")[1] self._stop_cmd = commands_set.split("_")[2] self._timer_start = time.time() - self._previous_state = self._stop_cmd + self._state = self._stop_cmd + self._previous_state = self._state + self._current_cover_position = 0 print("Initialized cover [{}]".format(self.name)) @property @@ -120,20 +121,19 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" self.debug("Setting cover position: %r", kwargs[ATTR_POSITION]) - if self._config[CONF_POSITIONING_MODE] == COVER_MODE_FAKE: + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: newpos = float(kwargs[ATTR_POSITION]) currpos = self.current_cover_position posdiff = abs(newpos - currpos) - mydelay = posdiff / 50.0 * self._config[CONF_SPAN_TIME] + mydelay = posdiff / 100.0 * self._config[CONF_SPAN_TIME] if newpos > currpos: self.debug("Opening to %f: delay %f", newpos, mydelay) await self.async_open_cover() else: self.debug("Closing to %f: delay %f", newpos, mydelay) await self.async_close_cover() - await asyncio.sleep(mydelay) - await self.async_stop_cover() + self.hass.async_create_task(self.async_stop_after_timeout(mydelay)) self.debug("Done") elif self._config[CONF_POSITIONING_MODE] == COVER_MODE_POSITION: @@ -146,21 +146,50 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): converted_position, self._config[CONF_SET_POSITION_DP] ) + async def async_stop_after_timeout(self, delay_sec): + """Stop the cover if timeout (max movement span) occurred.""" + await asyncio.sleep(delay_sec) + await self.async_stop_cover() + async def async_open_cover(self, **kwargs): """Open the cover.""" self.debug("Launching command %s to cover ", self._open_cmd) await self._device.set_dp(self._open_cmd, self._dp_id) + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + # for timed positioning, stop the cover after a full opening timespan + # instead of waiting the internal timeout + self.hass.async_create_task( + self.async_stop_after_timeout( + self._config[CONF_SPAN_TIME] + COVER_TIMEOUT_TOLERANCE + ) + ) async def async_close_cover(self, **kwargs): """Close cover.""" self.debug("Launching command %s to cover ", self._close_cmd) await self._device.set_dp(self._close_cmd, self._dp_id) + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + # for timed positioning, stop the cover after a full opening timespan + # instead of waiting the internal timeout + self.hass.async_create_task( + self.async_stop_after_timeout( + self._config[CONF_SPAN_TIME] + COVER_TIMEOUT_TOLERANCE + ) + ) async def async_stop_cover(self, **kwargs): """Stop the cover.""" self.debug("Launching command %s to cover ", self._stop_cmd) await self._device.set_dp(self._stop_cmd, self._dp_id) + def status_restored(self, stored_state): + """Restore the last stored cover status.""" + if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED: + stored_pos = stored_state.attributes.get("current_position") + if stored_pos is not None: + self._current_cover_position = stored_pos + self.debug("Restored cover position %s", self._current_cover_position) + def status_updated(self): """Device status was updated.""" self._previous_state = self._state @@ -177,13 +206,13 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): else: self._current_cover_position = curr_pos if ( - self._config[CONF_POSITIONING_MODE] == COVER_MODE_FAKE + self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED and self._state != self._previous_state ): if self._previous_state != self._stop_cmd: # the state has changed, and the cover was moving time_diff = time.time() - self._timer_start - pos_diff = round(time_diff / self._config[CONF_SPAN_TIME] * 50.0) + pos_diff = round(time_diff / self._config[CONF_SPAN_TIME] * 100.0) if self._previous_state == self._close_cmd: pos_diff = -pos_diff self._current_cover_position = min( @@ -197,6 +226,7 @@ class LocaltuyaCover(LocalTuyaEntity, CoverEntity): time_diff, pos_diff, ) + # store the time of the last movement change self._timer_start = time.time() diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index b552bc0..6ce233a 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -51,10 +51,10 @@ "voltage": "Voltage", "commands_set": "Open_Close_Stop Commands Set", "positioning_mode": "Positioning mode", - "current_position_dp": "Current Position (when Position mode is *position*)", - "set_position_dp": "Set Position (when Position Mode is *position*)", - "position_inverted": "Invert 0-100 position (when Position Mode is *position*)", - "span_time": "Full opening time, in secs. (when Position Mode is fake*)", + "current_position_dp": "Current Position (for *position* mode only)", + "set_position_dp": "Set Position (for *position* mode only)", + "position_inverted": "Invert 0-100 position (for *position* mode only)", + "span_time": "Full opening time, in secs. (for *timed* mode only)", "unit_of_measurement": "Unit of Measurement", "device_class": "Device Class", "scaling": "Scaling Factor", @@ -105,8 +105,8 @@ "positioning_mode": "Positioning mode", "current_position_dp": "Current Position (for *position* mode only)", "set_position_dp": "Set Position (for *position* mode only)", - "position_inverted": "Invert 0-100 position (when Position Mode is *position*)", - "span_time": "Full opening time, in secs. (for *fake* mode only)", + "position_inverted": "Invert 0-100 position (for *position* mode only)", + "span_time": "Full opening time, in secs. (for *timed* mode only)", "unit_of_measurement": "Unit of Measurement", "device_class": "Device Class", "scaling": "Scaling Factor", diff --git a/info.md b/info.md index d0a2c76..3c69026 100644 --- a/info.md +++ b/info.md @@ -55,10 +55,10 @@ localtuya: friendly_name: Device Cover id: 2 open_close_cmds: ["on_off","open_close"] # Optional, default: "on_off" - positioning_mode: ["none","position","fake"] # Optional, default: "none" + positioning_mode: ["none","position","timed"] # Optional, default: "none" currpos_dps: 3 # Optional, required only for "position" mode setpos_dps: 4 # Optional, required only for "position" mode - span_time: 25 # Full movement time: Optional, required only for "fake" mode + span_time: 25 # Full movement time: Optional, required only for "timed" mode - platform: fan friendly_name: Device Fan