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

Rewrite BulbDevice and rework set_multiple_values() #525

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

uzlonewolf
Copy link
Collaborator

This is a pretty big rewrite to BulbDevice. The fact that it connects to the bulb and calls status() as soon as the version is set has always bugged me, so now it delays that until a command is sent. If a user calls status() themselves before they send a command then it uses the result from that status call instead of calling it again.

Also included are modifications to core: a caching mechanism has been added so devices can try to use cached DP values instead of making calls to the device, and set_multiple_values() has been modified to try and detect devices which fall over if they get sent multiple DPs in a single request. The caching only works when an active persistent connection is open; closing the connection clears the cache since we won't get notified when DPs change.

To access the cache, call d.cached_status(nowait). When nowait=True then the cached value is returned if it is available or None if it is not. When nowait=False then the cached value is returned if it is available or a call to d.status() is made if it is not.

BulbDevice now leans on the cache to try and minimize the number of DPs set in set_white() and set_colour(). This combined with the set_multiple_values() change is to help with bulbs that only accept 1 DP at a time, such as @Anonymouse83's in #504.

Todo: update documentation.

result = super(BulbDevice, self).status(nowait=nowait)
if result and (not self.bulb_type) and (self.DPS in result):
self.detect_bulb(result)
return result
Copy link
Owner

Choose a reason for hiding this comment

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

I like this! clean.

for rgb_value in rgb:
value = int(rgb_value)
if value < 0 or value > 255:
raise ValueError(f"Bulb type {bulb} must have RGB values 0-255.")
Copy link
Owner

Choose a reason for hiding this comment

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

This is needed, but it occurs to me that we have a real mix of using python exceptions versus returning error payloads, or actually not doing any validation. This is more of a commentary on some of our global style/design we could address rather than specific to this PR. :)

if len(hexvalue_hsv) == 7:
hexvalue = hexvalue + "0" + hexvalue_hsv
else:
hexvalue = hexvalue + "00" + hexvalue_hsv
Copy link
Owner

Choose a reason for hiding this comment

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

I love that we are addressing this. This is original pytuya code and more complicated than it needs to be. Thank you.

self.has_colour = has_colour
elif self.has_colour is None:
self.has_colour = bool(self.DPS_INDEX_COLOUR[self.bulb_type])

Copy link
Owner

Choose a reason for hiding this comment

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

❤️ this

data = self._send_receive(payload, getresponse=(not nowait))
return data

return self.set_colour( r * 255.0, g * 255.0, b * 255.0, nowait=nowait )
Copy link
Owner

Choose a reason for hiding this comment

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

Also, clean API change. I think there is enough API change here we need to signal breaking change with the version number we pick.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think there are any API changes? Most of this is function de-duplication and code cleanup. I'll double-check but I think the only user-visible changes are 1) manually reading d.bulb_type will read None until either d.status() is called or a command is sent 2) unknown bulbs now default to type 'B' instead of 'A' and 3) a force argument has been added to set_colour() and set_white() to disable some of the new optimizations, however this feels like a dirty hack to me and I'm probably going to rework it to remove this.

Copy link
Owner

Choose a reason for hiding this comment

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

Fair. Those are minor. Thinking of old code out there that may be impacted by those two things is very edge case. I'm good with a minor update. I had a stronger impression of the change when I was originally doing the review, but feel less concerned now.

payload = {
self.DPS_INDEX_BRIGHTNESS[self.bulb_type]: brightness,
self.DPS_INDEX_COLOURTEMP[self.bulb_type]: colourtemp,
}
Copy link
Owner

Choose a reason for hiding this comment

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

Instead of payload should we move to dps_values or something like that to not confuse this with raw "payloads"?

# for colour mode use hsv to increase brightness
if self.bulb_type == "A":
value = brightness / 255.0
else:
value = brightness / 1000.0
(h, s, v) = self.colour_hsv()
data = self.set_hsv(h, s, value, nowait=nowait)
msg += 'No repsonse from bulb.'
else:
msg += "Device mode is not 'white' or 'colour', cannot set brightness."
Copy link
Owner

Choose a reason for hiding this comment

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

What about?...

f"Unable to set brightness, device mode needs to be 'white' or 'colour' but reports {state["mode"]}"


if data is not None or nowait is True:
return data
else:
return error_json(ERR_STATE, "set_brightness: Unknown bulb state.")
log.debug( msg )
return error_json(ERR_STATE, msg)
Copy link
Owner

@jasonacox jasonacox Aug 1, 2024

Choose a reason for hiding this comment

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

I mentioned how we use python exceptions in some cases and error responses in other place. But thinking about it, it seems we do this:

  • If user provides invalid parameters (values) - raise exception
  • If buld responds with error or invalid conditions - respond with error_json()

I think that makes sense. We may need to do scan the rest of our code to ensure that is consistent.

return BulbDevice._hexvalue_to_hsv(hexvalue, self.bulb_type)

def state(self):
"""Return state of Bulb"""
status = self.status()
status = self.cached_status()
Copy link
Owner

Choose a reason for hiding this comment

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

I like the idea of caching. But what if the user is making changes to the device using the SmartLife app or other tool?

  • We should definitely cache the device type and capabilities.
  • Status or current state of device should be live (unless we provide a force=False)?

Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But what if the user is making changes to the device using the SmartLife app or other tool?

Every device I've seen sends asynchronous updates when that happens, so as long as the user calls .receive() every now and then or sends a command with nowait=False then those changes will be picked up and cached. This is also why caching is only enabled when persist=True and an active connection to the device is open.

Copy link
Owner

Choose a reason for hiding this comment

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

Sold.

tinytuya/core.py Outdated
@@ -820,7 +820,7 @@ def assign_dp_mappings( tuyadevices, mappings ):

class XenonDevice(object):
def __init__(
self, dev_id, address=None, local_key="", dev_type="default", connection_timeout=5, version=3.1, persist=False, cid=None, node_id=None, parent=None, connection_retry_limit=5, connection_retry_delay=5, port=TCPPORT # pylint: disable=W0621
self, dev_id, address=None, local_key="", dev_type="default", connection_timeout=5, version=3.1, persist=False, cid=None, node_id=None, parent=None, connection_retry_limit=5, connection_retry_delay=5, port=TCPPORT, max_simultaneous_dps=0 # pylint: disable=W0621
Copy link
Owner

Choose a reason for hiding this comment

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

Should caching be an option (that can be turned off) in addition to persist=True?

Also, we should break up the super long line with some \n love.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't see a point in disabling it. Cached data is only returned with .cached_status() so if you don't want cached data then just call .status(). Enabling it without persist=True is going to cause the cached state to not match the device state if someone presses a button on the device or makes a change in the SmartLife app.

@jasonacox
Copy link
Owner

I think this is a good update @uzlonewolf - ready for non-draft PR IMHO.

@uzlonewolf
Copy link
Collaborator Author

@jasonacox Do you happen to have an example status() response for a Type C bulb? I want to check some of my logic for that bulb type before committing this.

@jasonacox
Copy link
Owner

Hi @uzlonewolf , makes sense. However, I don't seem to have one. The Type C Bulb addition came from #54 by @davidlie (who may be able to test for us?) and I recall it was for the Feit Electric Smart Light Bulb. I see some on Amazon but not sure if that would be the C type. I thought I picked one up but scanned my devices and no Type C's. 😞

@uzlonewolf
Copy link
Collaborator Author

Looking at that PR it looks to be a dimmer switch and not a bulb at all. Using the Internet Archive I found some that look like they're probably it and ordered a couple new-old-stock off ebay. We'll see in a few days.

@jasonacox
Copy link
Owner

Yes, I can't recall why it said dimmer switch but PR was for a smart bulb. It wasn't that many years ago, but long enough fo rme to completely forget. 🤷 Thanks for doing the research. 🙏

@uzlonewolf
Copy link
Collaborator Author

My dimmer switch came in Saturday and it does identify as a Type C bulb. When it comes down to it, is there really a difference between a dimmer and a single-color (non-CCT) bulb? Anyway, I also have some single-color as well as CCT bulbs arriving today.

The more I think about it the less happy I am with the entire "Type X" thing and am probably going to rewrite it to just use "has feature" (mode, brightness, colortemp, etc). I may or may not try and use the DPS mapping (if it's available) instead of blindly guessing based off DP ID.

@jasonacox
Copy link
Owner

Yes, I haven't been fond of the Type X thing either, but I don't know of a better way to represent both the DPS mapping as well as the range differences.

Convert an RGB value to the hex representation expected by Tuya Bulb.
Index (DPS_INDEX_COLOUR) is assumed to be in the format:
(Type A) Index: 5 in hex format: rrggbb0hhhssvv
(Type B) Index: 24 in hex format: hhhhssssvvvv
While r, g and b are just hexadecimal values of the corresponding
Red, Green and Blue values, the h, s and v values (which are values
between 0 and 1) are scaled:
Type A: 360 (h) and 255 (s and v)
Type B: 360 (h) and 1000 (s and v)

@uzlonewolf
Copy link
Collaborator Author

Last week I ended up rewriting BulbDevice and almost completely eliminated the Type X thing. I left it in a few places for backwards compatibility and as a quick way to manually set the DP range, but it's otherwise gone. The RGB/HSV range is now selected by a boolean, and the "has X capability" is set by whether a DP is defined or not. I'll push an update once I have the DP detection finished.

I picked up an assortment of bulbs to test with. The new Feit single-color white bulbs are v3.5 and act like what was formerly known as Type B but with a few DPs missing. The older Feit dimmer uses DPs 1-3 with 3 being the "minimum dim" setting, while Geeni bi-color/CCT white only bulbs use DPs 1-3 with 3 being CCT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants