Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cover fake position persistent #213

Merged
merged 15 commits into from
Dec 30, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions custom_components/localtuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
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
Expand Down
16 changes: 14 additions & 2 deletions custom_components/localtuya/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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 (
Expand Down Expand Up @@ -198,7 +198,7 @@ def disconnected(self):
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):
Expand All @@ -209,13 +209,19 @@ def __init__(self, device, config_entry, dp_id, logger, **kwargs):
self._dp_id = dp_id
self._status = {}
self.set_logger(logger, self._config_entry.data[CONF_DEVICE_ID])
self._stored_state = {}

async def async_added_to_hass(self):
"""Subscribe localtuya events."""
await super().async_added_to_hass()

self.debug("Adding %s with configuration: %s", self.entity_id, self._config)

state = await self.async_get_last_state()
if state:
self._stored_state = state
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really have to store it in self._stored_state, can't we just pass the value as an argument to status_restored?

self.status_restored()

def _update_handler(status):
"""Update entity state when status was updated."""
if status is not None:
Expand Down Expand Up @@ -302,3 +308,9 @@ def status_updated(self):

Override in subclasses and update entity specific state.
"""

def status_restored(self):
"""Device status was restored.

Override in subclasses and update entity specific state.
"""
49 changes: 38 additions & 11 deletions custom_components/localtuya/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
COVER_12_CMDS = "1_2_3"
COVER_MODE_NONE = "none"
COVER_MODE_POSITION = "position"
COVER_MODE_FAKE = "fake"
COVER_MODE_TIMED = "timed"

DEFAULT_COMMANDS_SET = COVER_ONOFF_CMDS
DEFAULT_POSITIONING_MODE = COVER_MODE_NONE
Expand All @@ -47,7 +47,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),
Expand All @@ -64,16 +64,16 @@ 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]
self._open_cmd = commands_set.split("_")[0]
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
Expand Down Expand Up @@ -120,20 +120,19 @@ def is_closed(self):
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:
Expand All @@ -146,21 +145,48 @@ async def async_set_cover_position(self, **kwargs):
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] + 5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic constant: what does 5 mean? Create a constant for it.

)

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] + 5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

)

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):
"""Restore the last stored cover status."""
if self._config[CONF_POSITIONING_MODE] == COVER_MODE_TIMED:
stored_pos = int(
self._stored_state.attributes.get("current_position", "-1")
)
if stored_pos != -1:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You return the string "-1" if the variable does not exist but compare with an integer here, is that intentional? Maybe just check if not None instead and skip default value?

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
Expand All @@ -177,13 +203,13 @@ def status_updated(self):
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(
Expand All @@ -197,6 +223,7 @@ def status_updated(self):
time_diff,
pos_diff,
)

# store the time of the last movement change
self._timer_start = time.time()

Expand Down
12 changes: 6 additions & 6 deletions custom_components/localtuya/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,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",
Expand Down Expand Up @@ -103,8 +103,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",
Expand Down