make-all / tuya-local

Local support for Tuya devices in Home Assistant
MIT License
1.07k stars 435 forks source link

Add support for Zedar R600 #2072

Open lankhaar opened 5 days ago

lankhaar commented 5 days ago

Describe the bug After a clean install of tuya-local (and a fairly clean install of HA), I can't find the device I expected to find. I fill in all data (ip, protocol 3.3, local key) for my device (a robot vacuum cleaner) and the dps that are logged are in fact the dps I would expect from my robot vacuum so no issues there it seems, yet it will always suggest the same 6 devices (see screenshot) with none of them being a robot vacuum.

image

I have also added my own yaml file with my device id and configured all dps (it's almost identical to the device ID kljjvealoaepeysi, device "Kabum Smart 700 Rev 2023").

All the dps I find in the log match the one from the yaml file, yet it's not suggested as a device type.

Additional context Logs: image

Device matches asc_wifi_circuit_breaker with quality of 19%. DPS: {"updated_at": 1719961775.8133426, "1": false, "2": false, "4": "smart", "5": "sleep", "6": 0, "7": 0, "8": 83, "9": "strong", "17": 7408, "19": 12808, "21": 7408, "23": 13678, "26": 4, "27": true, "28": 0, "45": true, "102": false, "105": false, "106": false, "107": false, "108": false}
Detected blocking call to open inside the event loop by custom integration 'tuya_local' at custom_components/tuya_local/helpers/device_config.py, line 101: self._config = load_yaml(filename) (offender: /usr/src/homeassistant/homeassistant/util/yaml/loader.py, line 226: with open(fname, encoding="utf-8") as conf_file:), please create a bug report at https://github.com/make-all/tuya-local/issues Traceback (most recent call last): File "<frozen runpy>", line 198, in _run_module_as_main File "<frozen runpy>", line 88, in _run_code File "/usr/src/homeassistant/homeassistant/__main__.py", line 223, in <module> sys.exit(main()) File "/usr/src/homeassistant/homeassistant/__main__.py", line 209, in main exit_code = runner.run(runtime_conf) File "/usr/src/homeassistant/homeassistant/runner.py", line 190, in run return loop.run_until_complete(setup_and_run_hass(runtime_config)) File "/usr/local/lib/python3.12/asyncio/base_events.py", line 672, in run_until_complete self.run_forever() File "/usr/local/lib/python3.12/asyncio/base_events.py", line 639, in run_forever self._run_once() File "/usr/local/lib/python3.12/asyncio/base_events.py", line 1988, in _run_once handle._run() File "/usr/local/lib/python3.12/asyncio/events.py", line 88, in _run self._context.run(self._callback, *self._args) File "/usr/local/lib/python3.12/site-packages/aiohttp/web_protocol.py", line 452, in _handle_request resp = await request_handler(request) File "/usr/local/lib/python3.12/site-packages/aiohttp/web_app.py", line 543, in _handle resp = await handler(request) File "/usr/local/lib/python3.12/site-packages/aiohttp/web_middlewares.py", line 114, in impl return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/security_filter.py", line 92, in security_filter_middleware return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/forwarded.py", line 83, in forwarded_middleware return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/request_context.py", line 26, in request_context_middleware return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/ban.py", line 85, in ban_middleware return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/auth.py", line 242, in auth_middleware return await handler(request) File "/usr/src/homeassistant/homeassistant/components/http/headers.py", line 32, in headers_middleware response = await handler(request) File "/usr/src/homeassistant/homeassistant/helpers/http.py", line 73, in handle result = await handler(request, **request.match_info) File "/usr/src/homeassistant/homeassistant/components/http/decorators.py", line 81, in with_admin return await func(self, request, *args, **kwargs) File "/usr/src/homeassistant/homeassistant/components/config/config_entries.py", line 222, in post return await super().post(request, flow_id) File "/usr/src/homeassistant/homeassistant/components/http/data_validator.py", line 70, in wrapper return await method(view, request, data, *args, **kwargs) File "/usr/src/homeassistant/homeassistant/helpers/data_entry_flow.py", line 122, in post result = await self._flow_mgr.async_configure(flow_id, data) File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 368, in async_configure result = await self._async_configure(flow_id, user_input) File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 414, in _async_configure result = await self._async_handle_step( File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 517, in _async_handle_step result: _FlowResultT = await getattr(flow, method)(user_input) File "/config/custom_components/tuya_local/config_flow.py", line 436, in async_step_local return await self.async_step_select_type() File "/config/custom_components/tuya_local/config_flow.py", line 474, in async_step_select_type async for type in self.device.async_possible_types(): File "/config/custom_components/tuya_local/device.py", line 360, in async_possible_types for match in possible_matches(cached_state): File "/config/custom_components/tuya_local/helpers/device_config.py", line 982, in possible_matches parsed = TuyaDeviceConfig(cfg) File "/config/custom_components/tuya_local/helpers/device_config.py", line 101, in __init__ self._config = load_yaml(filename)
lankhaar commented 4 days ago

@make-all , judging on the tags you've added/removed I don't think I made myself 100% clear.

The id that I mentioned (for Kabum Smart 700 Rev 2023) was just an example, but for me the search seems broken entirely, I can't seem to find ANY product. The search result will always be the same (19% match on asc_wifi_circuit_breaker).

To clarify what I said in the issue itself; no matter the input I provide, I will never enter the else clause in the following code nor will I get actual usefull results. The result I get appear to be static. https://github.com/make-all/tuya-local/blob/80b014167f436147a31e934f5b57715258fbfcff/custom_components/tuya_local/config_flow.py#L492-L505

I hope that clears things up now :-)

make-all commented 4 days ago

You are correct that you are not making yourself clear. You say "I can't seem to find ANY product", but your screenshot shows six possible matches found. And now your complaint seems to be that you are not able to enter the else clause, which is the "can't find ANY product" case.

lankhaar commented 4 days ago

@make-all , I'm sorry for all the confusion, let me start over (I've updated the OP in case other people read this issue and get confused as well, so I recommend you read that again since all details are there);

In my very specific case, I want to add a robot vacuum; brand Zedar, model R600 (see Zedar R600). I added my own yaml file for this (which looks a lot like the one from "Kabum Smart 700 Rev 2023" and I'll make a PR for it once I've got it to work).

Now the problem: when I connect to the device, I see indeed 6 devices, but not the device I expected to find, even though the dps I see in the logs match with the ones in the yaml.

make-all commented 4 days ago

There is a script utils/match_against.py which you can use at the command line (after setting up your python environment to include all the requirements-dev.txt requirements, and including the tuya-local base directory in your PYTHONPATH). It takes two arguments: the name of the config file to match against, and the dps list in the json format output in the logs (enclose it in single quotes to ensure it goes through as a single argument)

lankhaar commented 4 days ago

That was really helpfull, thanks @make-all !

This helped me doing some debugging and I managed to add the device now. I'm only left with a few more issues. Perhaps you could guide me into the right direction?

The vacuum is in an idle state, but here there's some sort of state which says error, however I'm not even sure what that card entry is; image

Also none of the commands work yet. I'll continue my troubleshooting, but I'd love to hear if you've got an idea :-D

lankhaar commented 4 days ago

I'm almost there, but I'm gonna need help with the final part. I think I've mapped all data, everything is showing correctly (except for that error from previous comment, but I'm not sure what you'd need for that). From what I can see my issues are only in the action (sending it out to clean, sending it to it's dock, etc.). On every action change, Tuya seems to be doing 3 things.. Changing the status (DP ID 5), changing the mode (DP ID 4), changing the power_go (DP ID 1)

image

Hope you can help!

Device ID

yiarxtixzkmnegxk

YAML

name: Robot vacuum
products:
  - id: yiarxtixzkmnegxk
    name: Zedar R600
primary_entity:
  entity: vacuum
  dps:
    - id: 1
      type: boolean
      optional: true
      name: activate
      mapping:
        - dps_val: false
          constraint: pause
          conditions:
            - dps_val: true
              value: false
            - dps_val: false
              value: false
              hidden: true
            - dps_val: null
              value: false
              hidden: true
        - dps_val: true
          constraint: pause
          conditions:
            - dps_val: false
              value: true
            - dps_val: true
              value: true
              hidden: true
            - dps_val: null
              value: true
              hidden: true
        - dps_val: null
          value: false
          hidden: true
    - id: 2
      type: boolean
      optional: true
      name: pause
    - id: 3
      name: return_to_base
      type: boolean
      optional: true
    - id: 4
      type: string
      name: command
      mapping:
        - dps_val: smart
          value: smart
        - dps_val: chargego
          value: return_to_base
        - dps_val: zone
          value: zone
        - dps_val: pose
          value: go_to_position
        - dps_val: part
          value: room
    - id: 5
      type: string
      name: status
      mapping:
        - dps_val: standby
          value: standby
        - dps_val: smart
          value: cleaning
        - dps_val: smart_clean
          value: cleaning
        - dps_val: zone_clean
          value: cleaning
        - dps_val: part_clean
          value: cleaning
        - dps_val: cleaning
          value: cleaning
        - dps_val: paused
          value: paused
        - dps_val: goto_pos
          value: going_to_location
        - dps_val: pos_arrived
          value: on_location
        - dps_val: pos_unarrive
          value: cannot_find_location
        - dps_val: goto_charge
          value: returning
        - dps_val: charging
          value: charging
        - dps_val: charge_done
          value: charged
        - dps_val: sleep
          value: sleep
        - dps_val: select_room
          value: select_room
        - dps_val: wall_follow
          value: wall_follow
        - dps_val: direction_control
          value: direction_control
    - id: 9
      type: string
      name: fan_speed
      mapping:
        - dps_val: turnoff
          value: "Off"
        - dps_val: gentle
          value: Low
        - dps_val: normal
          value: Medium
        - dps_val: strong
          value: High
    - id: 11
      type: boolean
      optional: true
      name: locate
    - id: 12
      type: string
      optional: true
      name: direction_control
      mapping:
        - dps_val: forward
          value: forward
        - dps_val: backward
          value: reverse
        - dps_val: turn_left
          value: left
        - dps_val: turn_right
          value: right
        - dps_val: stop
          value: stop
    - id: 14
      type: string
      optional: true
      name: path_data
    - id: 15
      type: base64
      optional: true
      name: complex_command
    - id: 16
      type: string
      optional: true
      name: request
      mapping:
        - dps_val: get_map
          value: Get map
        - dps_val: get_path
          value: Get path
        - dps_val: get_both
          value: Get both
    - id: 28
      type: bitfield
      name: error
      mapping:
        - dps_val: 2
          value: left_wheel_hang
        - dps_val: 4
          value: main_brush_stuck
        - dps_val: 8
          value: right_wheel_hang
        - dps_val: 16
          value: left_wheel_stuck
        - dps_val: 32
          value: right_wheel_stuck
        - dps_val: 64
          value: cliff
        - dps_val: 128
          value: side_brush_stuck
        - dps_val: 512
          value: collision
        - dps_val: 1024
          value: dust_box_stuck
        - dps_val: 4096
          value: ground_check_failure
        - dps_val: 32768
          value: vacuum_motor_stuck
        - dps_val: 65536
          value: high_current
        - dps_val: 131072
          value: charge_port_closed
    - id: 32
      type: string
      optional: true
      name: device_timer
    - id: 33
      type: string
      optional: true
      name: disturb_time_set
    - id: 34
      type: string
      optional: true
      name: device_info
    - id: 35
      type: string
      optional: true
      name: voice_data
    - id: 36
      name: language
      type: string
      optional: true
secondary_entities:
#  - entity: button
#    name: Start
#    icon: "mdi:play"
#    category: config
#    dps:
#      - id: 1
#        type: boolean
#        name: button
#        optional: true
#  - entity: button
#    name: Pause
#    category: config
#    icon: "mdi:pause"
#    dps:
#      - id: 2
#        type: boolean
#        name: button
#        optional: true
#  - entity: button
#    name: Dock
#    category: config
#    icon: "mdi:lightning-bolt"
#    dps:
#      - id: 3
#        type: boolean
#        name: button
#        optional: true
  - entity: sensor
    name: Cleaning time
    class: duration
    category: diagnostic
    dps:
      - id: 6
        type: integer
        name: sensor
        unit: min
        class: measurement
  - entity: sensor
    name: Cleaned area
    category: diagnostic
    icon: "mdi:floor-plan"
    dps:
      - id: 7
        type: integer
        name: sensor
        unit: m2
  - entity: sensor
    class: battery
    dps:
      - id: 8
        type: integer
        name: sensor
        unit: "%"
        class: measurement
  - entity: select
    name: Mopping
    icon: "mdi:cup-water"
    category: config
    dps:
      - id: 10
        type: string
        name: option
        optional: true
        mapping:
          - dps_val: closed
            value: "Off"
          - dps_val: low
            value: Low
          - dps_val: middle
            value: Medium
          - dps_val: high
            value: High
          - dps_val: null
            value: "Off"
            hidden: true
  - entity: button
    name: Reset map
    category: config
    dps:
      - id: 13
        type: boolean
        name: button
        optional: true
  - entity: sensor
    name: Edge brush life
    class: duration
    category: diagnostic
    dps:
      - id: 17
        type: integer
        name: sensor
        unit: min
  - entity: button
    name: Edge brush reset
    class: restart
    category: config
    dps:
      - id: 18
        type: boolean
        name: button
        optional: true
  - entity: sensor
    name: Roll brush life
    class: duration
    category: diagnostic
    dps:
      - id: 19
        type: integer
        name: sensor
        unit: min
  - entity: button
    name: Roll brush reset
    class: restart
    category: config
    dps:
      - id: 20
        type: boolean
        name: button
        optional: true
  - entity: sensor
    name: Filter life
    class: duration
    category: diagnostic
    dps:
      - id: 21
        type: integer
        name: sensor
        unit: min
  - entity: button
    name: Filter reset
    class: restart
    category: config
    dps:
      - id: 22
        type: boolean
        name: button
        optional: true
  - entity: sensor
    name: Mop life
    class: duration
    category: diagnostic
    dps:
      - id: 23
        type: integer
        name: sensor
        unit: min
  - entity: button
    name: Mop reset
    class: restart
    category: config
    dps:
      - id: 24
        type: boolean
        name: button
        optional: true
  - entity: switch
    name: Do not disturb
    category: config
    dps:
      - id: 25
        type: boolean
        name: switch
        optional: true
        mapping:
          - dps_val: null
            value: false
            hidden: true
  - entity: number
    name: Volume
    category: config
    icon: "mdi:volume-high"
    dps:
      - id: 26
        type: integer
        name: value
        range:
          min: 0
          max: 10
        unit: "%"
        mapping:
          - scale: 0.1
lankhaar commented 3 days ago

@make-all , I can tell by the amount of "new device requests" that you're a busy man, so I'm happy to add support for this myself. It would be amazing if you could push me into the direction on how to debug though, because it could take me days finding it out on my own lol.

If I can get the commands to work, that would be amazing already. I already shared what's needed for that in my last comment.

Currently I have to get 2 things done I guess. When play button is hit, I should update the command to "smart" and switch power_go (aka activate) to true. I assume this is done through the mapping on the command? Would that look something like the following snippet? Or should this be configured on it's primary entity instead of on the command?

  - entity: button
    name: Start
    icon: "mdi:play"
    category: config
    dps:
      - id: 1
        type: boolean
        name: button
        optional: true
        mapping:
          - dps_val: true
            constraint: command
            value: smart

Also I see one weird thing in the activate command. If I click the play button, you'll see that it will turn itself off again, I assume because it updates data from the device which never turned on. Not sure whether that is or is not intended. Could be intended because it technically never turns on (it never starts executing any kind of command).

https://github.com/make-all/tuya-local/assets/107768491/f9471125-ffae-47da-861b-81de68def70e

I do not see any messages in the log either, is there some kind of way to debug this? Maybe a way to see a response from the device after sending it a command?

lankhaar commented 2 days ago

Hi @make-all , sorry for pinging you again.

I've finished the mapping so all data is showing correctly (inculding the error I mentioned 2 comment earlier).

Now I'm only stuck with the commands not working. I did my research and I'm fairly certain that my entity is configured correctly, but my device doesn't really seem to pickup the commands it should recieve. I have enabled debug logs and here's what I've got:

2024-07-05 16:26:09.587 DEBUG (MainThread) [custom_components.tuya_local.helpers.device_config] Robot vacuum: Mapped dps 1 to True from True
2024-07-05 16:26:09.587 DEBUG (MainThread) [custom_components.tuya_local.helpers.device_config] Considering mapping for range of activate
2024-07-05 16:26:09.588 DEBUG (MainThread) [custom_components.tuya_local.device] Zedar vacuum new pending updates: {"2": {"value": false, "updated_at": 1720189569.5880642, "sent": false}, "1": {"value": true, "updated_at": 1720189569.5880642, "sent": false}}
2024-07-05 16:26:09.589 DEBUG (MainThread) [custom_components.tuya_local.device] Zedar vacuum sending dps update: {"1": true, "2": false}
2024-07-05 16:26:09.705 DEBUG (MainThread) [custom_components.tuya_local.device] Zedar vacuum received {"1": false, "2": true, "4": "chargego", "5": "charge_done", "6": 0, "7": 0, "8": 100, "9": "strong", "17": 7371, "19": 12771, "21": 7371, "23": 13678, "26": 4, "27": false, "28": 0, "45": true, "102": false, "105": false, "106": false, "107": false, "108": false, "full_poll": true}

I'm not entirely sure what's happening here. It mentions it's sending the dps update as I expect it to send, but the dps aren't updated. Anything that you know of I can do?

lankhaar commented 1 day ago

Sorry didn't mean to close it...

@make-all , I can confirm that the commands I have configured do work (mapping is correct), but they never actually get to the device. I have wrote a custom script which worked instantly

 % cat zedar.py
import tinytuya
import sys

device = tinytuya.Device('DEVICE_ID', 'IP_ADDRESS', 'LOCAL_KEY')
device.set_version(3.3)
data = device.status()

request = sys.argv[1]

if (request == 'status'):
  print('Datapoints %r' % data)

if (request == 'start'):
  payload=device.generate_payload(tinytuya.CONTROL, {'1': True, '2': False})

if (request == 'pause'):
  payload=device.generate_payload(tinytuya.CONTROL, {'2': True})

if (request == 'charge'):
  payload=device.generate_payload(tinytuya.CONTROL, {'4': 'chargego'})

device._send_receive(payload)

With that script, I can run the following commands with no problem and the vacuum will follow up the commands;

python3 zedar.py start
python3 zedar.py pause
python3 zedar.py charge

Do you have any idea why these commands aren't received by the device if sent through your integration?