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

how can I get if set_value was successfull without recall status. #480

Open
Titriel opened this issue Apr 1, 2024 · 5 comments
Open

how can I get if set_value was successfull without recall status. #480

Titriel opened this issue Apr 1, 2024 · 5 comments
Labels
enhancement New feature or request

Comments

@Titriel
Copy link

Titriel commented Apr 1, 2024

Hi,
I am on creating an universal MQTT- Adapter.
I was able to add your tinytuya class in my Adapter, and it works fine.
Some devices are hard on the border of the range of my WIFI. So it would be helpfull, that calls like set_value, sends back if it was successtull sending data to the device. Or is there a way how I can requst this state ?

Thank You

@jasonacox
Copy link
Owner

Hi @Titriel - Can you give an example of your code? I think what you want is something like:

import tinytuya

# Connect
d = tinytuya.OutletDevice(id, ip, key, version="3.3")

# Set value of DPS index 25
d.set_value(25, 'xyz')

# Read value
data = d.status()
setting = data['dps']['25']
print(setting)

@Titriel
Copy link
Author

Titriel commented Apr 7, 2024

@jasonacox
Thank you very mutch for your answer. But this is that what I not want do do. Because I have to sent a second request to the device.
I have seen, it is possibel to do settings about retrys by setting up the OutletDevice eg. So I think you had figured out a posibility to check if a retry is needed or not. I think, if a request to the device ist sucsessfully, you stop retreying. Or did you only send the request n times in the maner of fire and forget ?
If you do fire and forget, that what I request to you is not posible. But if the device sands in any way an aknowlage back to an request, it should be posible eg. to return true by the call d.set_value if the device had aknowlaged and false if it has not aknowlaged over all retrys. If that will be posible, it makes sence do do this kind of return by all devices requsting calls.

Sorry for my terible english, it's not my native language.

@jasonacox
Copy link
Owner

Hi @Titriel - the commands wait for a response by default. You would need to set nowait=True for it to send only without waiting for a ACK response.

set_value(index, value, nowait=False) 

The set_value() function calls __send_receive(() which has logic to retry up to the socketRetryLimit amount and as long as the retry setting is set globally (default is True).

For your use case, you just need to make sure you get a valid response:

response = d.set_value("1",True)
# Should get eomething like: {'devId': 'abcdefg1234567', 'dps': {'1': True}, 't': 1712501649}
if "dps" in response:
    print("Updated")
else:
    print("Failed")

If you are curious, you can see how TinyTuya handles it in the core code here:

tinytuya/tinytuya/core.py

Lines 1141 to 1280 in 8971b80

def _send_receive(self, payload, minresponse=28, getresponse=True, decode_response=True, from_child=None):
"""
Send single buffer `payload` and receive a single buffer.
Args:
payload(bytes): Data to send. Set to 'None' to receive only.
minresponse(int): Minimum response size expected (default=28 bytes)
getresponse(bool): If True, wait for and return response.
"""
if self.parent:
return self.parent._send_receive(payload, minresponse, getresponse, decode_response, from_child=self)
if (not payload) and getresponse and self.received_wrong_cid_queue:
if (not self.children) or (not from_child):
r = self.received_wrong_cid_queue[0]
self.received_wrong_cid_queue = self.received_wrong_cid_queue[1:]
return r
found_rq = False
for rq in self.received_wrong_cid_queue:
if rq[0] == from_child:
found_rq = rq
break
if found_rq:
self.received_wrong_cid_queue.remove(found_rq)
return found_rq[1]
success = False
partial_success = False
retries = 0
recv_retries = 0
#max_recv_retries = 0 if not self.retry else 2 if self.socketRetryLimit > 2 else self.socketRetryLimit
max_recv_retries = 0 if not self.retry else self.socketRetryLimit
dev_type = self.dev_type
do_send = True
msg = None
while not success:
# open up socket if device is available
sock_result = self._get_socket(False)
if sock_result is not True:
# unable to get a socket - device likely offline
self._check_socket_close(True)
return error_json( sock_result if sock_result else ERR_OFFLINE )
# send request to device
try:
if payload is not None and do_send:
log.debug("sending payload")
enc_payload = self._encode_message(payload) if type(payload) == MessagePayload else payload
self.socket.sendall(enc_payload)
if self.sendWait is not None:
time.sleep(self.sendWait) # give device time to respond
if getresponse:
do_send = False
rmsg = self._receive()
# device may send null ack (28 byte) response before a full response
# consider it an ACK and do not retry the send even if we do not get a full response
if rmsg:
payload = None
partial_success = True
msg = rmsg
if (not msg or len(msg.payload) == 0) and recv_retries <= max_recv_retries:
log.debug("received null payload (%r), fetch new one - retry %s / %s", msg, recv_retries, max_recv_retries)
recv_retries += 1
if recv_retries > max_recv_retries:
success = True
else:
success = True
log.debug("received message=%r", msg)
else:
# legacy/default mode avoids persisting socket across commands
self._check_socket_close()
return None
except (KeyboardInterrupt, SystemExit) as err:
log.debug("Keyboard Interrupt - Exiting")
raise
except socket.timeout as err:
# a socket timeout occurred
if payload is None:
# Receive only mode - return None
self._check_socket_close()
return None
do_send = True
retries += 1
# toss old socket and get new one
self._check_socket_close(True)
log.debug(
"Timeout in _send_receive() - retry %s / %s",
retries, self.socketRetryLimit
)
# if we exceed the limit of retries then lets get out of here
if retries > self.socketRetryLimit:
log.debug(
"Exceeded tinytuya retry limit (%s)",
self.socketRetryLimit
)
# timeout reached - return error
return error_json(ERR_KEY_OR_VER)
# wait a bit before retrying
time.sleep(0.1)
except DecodeError as err:
log.debug("Error decoding received data - read retry %s/%s", recv_retries, max_recv_retries, exc_info=True)
recv_retries += 1
if recv_retries > max_recv_retries:
# we recieved at least 1 valid message with a null payload, so the send was successful
if partial_success:
self._check_socket_close()
return None
# no valid messages received
self._check_socket_close(True)
return error_json(ERR_PAYLOAD)
except Exception as err:
# likely network or connection error
do_send = True
retries += 1
# toss old socket and get new one
self._check_socket_close(True)
log.debug(
"Network connection error in _send_receive() - retry %s/%s",
retries, self.socketRetryLimit, exc_info=True
)
# if we exceed the limit of retries then lets get out of here
if retries > self.socketRetryLimit:
log.debug(
"Exceeded tinytuya retry limit (%s)",
self.socketRetryLimit
)
log.debug("Unable to connect to device ")
# timeout reached - return error
return error_json(ERR_CONNECT)
# wait a bit before retrying
time.sleep(0.1)
# except
# while
# could be None or have a null payload
if not decode_response:
# legacy/default mode avoids persisting socket across commands
self._check_socket_close()
return msg
return self._process_message( msg, dev_type, from_child, minresponse, decode_response )

@Titriel
Copy link
Author

Titriel commented Apr 17, 2024

Thank you very mutch. I check it out.

@uzlonewolf
Copy link
Collaborator

Unfortunately TinyTuya doesn't really handle command success/fail well. This is a good candidate for an enhancement. Let's take a step back and look at all the possible command results:

  1. Device completely ignores command and nothing is received
  2. Device completely ignores command but, by coincidence, an unrelated async update is received
  3. Device rejects command with an error message
  4. Device rejects command without an error message and nothing else is received
  5. Device rejects command without an error message and, by coincidence, an unrelated async update is received
  6. Device accepts command but nothing else is received
  7. Device accepts command and, either by coincidence or as a result of the command, an async update is received

The results for each might be unexpected:
1 will return an ERR_JSON object after the retry limit is reached.
2/5/7 will be counted as "success" and return that update.
3 will return an ERR_JSON about decoding the response.
4/6 will be counted as "success" and return None.

Instead of the above, we should probably be checking the retcode and immediately return an ERR_JSON if it's bad. Making sure we get a good retcode for the command we sent would be an additional check but brings up the issue of what to do with async updates received before the command response.

For now the only way to make sure we have success is to do something like:

d.set_value("1", True, nowait=True)

d.set_retry(False)
resp = d._send_receive(None, decode_response=False)
while resp and resp.cmd != [sent command]:
    resp = d._send_receive(None, decode_response=False)
d.set_retry(True)

if resp and resp.retcode == 0:
    print('Success!')
else:
    print('Failed!')

Unfortunately there's no good way of getting the exact command sent as it can be remapped based on the device quirks. Another TinyTuya enhancement could be to return the exact TuyaMessage sent when nowait=True.

@uzlonewolf uzlonewolf added the enhancement New feature or request label Apr 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants