Add Tuya Device Ventilation System (xfj)

I recently purchased a Pioneer wall mounted ERV ( Pioneer ECOasis 150). It utilizes a re-branded / re-skinned Tuya app, but I was able to successfully get it setup and working with the Tuya Smart Life app (by volcano technologies).

Using the Tuya integration, I am able to add the device to Home Assistant. The issue I am facing is that the Tuya device type (Ventilation System (xfj)-Tuya Developer Platform-Tuya Developer) is not supported by Home Assistant.

Scenes work, but I would prefer to control the device directly via HA. Please add support for the “xfj” device type in the Tuya integration.

Hello mox1,

There are at lease 2 custom_integrations available from Github using the HACS loader that may have what you want. You would have to research them and see…
TuyaLocal and LocalTuya.

Hello,
This is hard to do without the device diagnostics :).

Could you please provide them?

I’ll see what I can do from there…

1 Like

I too have one of these ventilation systems, here are my diagnostics

{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2024.9.2",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.12.4",
    "docker": true,
    "arch": "x86_64",
    "timezone": "America/New_York",
    "os_name": "Linux",
    "os_version": "6.6.46-haos",
    "supervisor": "2024.09.1",
    "host_os": "Home Assistant OS 13.1",
    "docker_version": "26.1.4",
    "chassis": "vm",
    "run_as_root": true
  },
  "custom_components": {},
  "integration_manifest": {
    "domain": "tuya",
    "name": "Tuya",
    "codeowners": [
      "Tuya",
      "zlinoliver",
      "frenck"
    ],
    "config_flow": true,
    "dependencies": [
      "ffmpeg"
    ],
    "dhcp": [
      {
        "macaddress": "105A17*"
      },
      {
        "macaddress": "10D561*"
      },
      {
        "macaddress": "1869D8*"
      },
      {
        "macaddress": "381F8D*"
      },
      {
        "macaddress": "508A06*"
      },
      {
        "macaddress": "68572D*"
      },
      {
        "macaddress": "708976*"
      },
      {
        "macaddress": "7CF666*"
      },
      {
        "macaddress": "84E342*"
      },
      {
        "macaddress": "D4A651*"
      },
      {
        "macaddress": "D81F12*"
      }
    ],
    "documentation": "https://www.home-assistant.io/integrations/tuya",
    "integration_type": "hub",
    "iot_class": "cloud_push",
    "loggers": [
      "tuya_iot"
    ],
    "requirements": [
      "tuya-device-sharing-sdk==0.1.9"
    ],
    "is_built_in": true
  },
  "setup_times": {},
  "data": {
    "endpoint": "https://apigw.tuyaus.com",
    "terminal_id": "1726850906678spsNUF",
    "mqtt_connected": true,
    "disabled_by": null,
    "disabled_polling": false,
    "id": "eb25edfdc9032d163d3d0m",
    "name": "ERVQ-H-F",
    "category": "xfj",
    "product_id": "gcaog2ut95yup99k",
    "product_name": "ERVQ-H-F",
    "online": true,
    "sub": false,
    "time_zone": "-04:00",
    "active_time": "2024-09-20T16:51:17+00:00",
    "create_time": "2024-09-20T16:51:17+00:00",
    "update_time": "2024-09-20T16:51:17+00:00",
    "function": {
      "switch": {
        "type": "Boolean",
        "value": {}
      }
    },
    "status_range": {
      "switch": {
        "type": "Boolean",
        "value": {}
      },
      "pm25": {
        "type": "Integer",
        "value": {
          "unit": "",
          "min": 0,
          "max": 1001,
          "scale": 0,
          "step": 1
        }
      },
      "eco2": {
        "type": "Integer",
        "value": {
          "unit": "",
          "min": 0,
          "max": 8001,
          "scale": 0,
          "step": 1
        }
      },
      "humidity_indoor": {
        "type": "Integer",
        "value": {
          "unit": "%",
          "min": 0,
          "max": 101,
          "scale": 0,
          "step": 1
        }
      },
      "temp_indoor": {
        "type": "Integer",
        "value": {
          "unit": "\u2109",
          "min": 0,
          "max": 200,
          "scale": 0,
          "step": 1
        }
      },
      "filter_life": {
        "type": "Integer",
        "value": {
          "unit": "\u5929",
          "min": 0,
          "max": 200,
          "scale": 0,
          "step": 1
        }
      },
      "fault": {
        "type": "Bitmap",
        "value": {
          "label": [
            "E1",
            "E2",
            "E3",
            "E4",
            "E5"
          ]
        }
      }
    },
    "status": {
      "switch": true,
      "pm25": 1,
      "eco2": 0,
      "humidity_indoor": 56,
      "temp_indoor": 73,
      "filter_life": 84,
      "fault": 0
    },
    "home_assistant": {
      "name": "ERVQ-H-F",
      "name_by_user": null,
      "disabled": false,
      "disabled_by": null,
      "entities": []
    },
    "set_up": false,
    "support_local": true
  }
}

Hello,
This should now be fully supported by my custom integration:

Please download the latest BETA version as this support has not made it yet in the stable release :).

Have a nice day,
Azerty

1 Like

Wow, it works! fantastic. Thank you so much for doing this, I really appreciate it.

Hello, I have a similar system , but with far more entities which are not fully recognized
Here are my diagnostic for an energy recovery ventilator ERV


{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2025.1.4",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.13.1",
    "docker": true,
    "arch": "aarch64",
    "timezone": "Europe/Rome",
    "os_name": "Linux",
    "os_version": "6.6.62-haos-raspi",
    "supervisor": "2024.12.3",
    "host_os": "Home Assistant OS 14.2",
    "docker_version": "27.2.0",
    "chassis": "embedded",
    "run_as_root": true
  },
  "custom_components": {
    "xtend_tuya": {
      "documentation": "https://github.com/azerty9971/xtend_tuya",
      "version": "3.0.8",
      "requirements": [
        "tuya-device-sharing-sdk==0.2.1",
        "tuya-iot-py-sdk==0.6.6"
      ]
    },
    "thermal_comfort": {
      "documentation": "https://github.com/dolezsa/thermal_comfort/blob/6af848f73d658eb7344f613dbcd097ea97573d18/README.md",
      "version": "2.2.2",
      "requirements": []
    },
    "hacs": {
      "documentation": "https://hacs.xyz/docs/use/",
      "version": "2.0.5",
      "requirements": [
        "aiogithubapi>=22.10.1"
      ]
    },
    "tuya_local": {
      "documentation": "https://github.com/make-all/tuya-local",
      "version": "2025.1.1",
      "requirements": [
        "tinytuya==1.15.1",
        "tuya-device-sharing-sdk~=0.2.1"
      ]
    },
    "alarmo": {
      "documentation": "https://github.com/nielsfaber/alarmo",
      "version": "v1.10.7",
      "requirements": []
    },
    "localtuya": {
      "documentation": "https://github.com/rospogrigio/localtuya/",
      "version": "5.2.3",
      "requirements": []
    },
    "dreame_vacuum": {
      "documentation": "https://github.com/Tasshack/dreame-vacuum",
      "version": "v1.0.4",
      "requirements": [
        "pillow",
        "numpy",
        "pybase64",
        "requests",
        "pycryptodome",
        "python-miio",
        "py-mini-racer",
        "tzlocal",
        "paho-mqtt"
      ]
    },
    "connectlife": {
      "documentation": "https://github.com/oyvindwe/connectlife-ha",
      "version": "0.22.1",
      "requirements": [
        "connectlife==0.5.3"
      ]
    },
    "adaptive_lighting": {
      "documentation": "https://github.com/basnijholt/adaptive-lighting#readme",
      "version": "1.25.0",
      "requirements": [
        "ulid-transform"
      ]
    }
  },
  "integration_manifest": {
    "domain": "xtend_tuya",
    "name": "Xtend Tuya",
    "after_dependencies": [
      "tuya"
    ],
    "codeowners": [
      "azerty9971"
    ],
    "config_flow": true,
    "dependencies": [
      "ffmpeg",
      "http"
    ],
    "dhcp": [
      {
        "macaddress": "105A17*"
      },
      {
        "macaddress": "10D561*"
      },
      {
        "macaddress": "1869D8*"
      },
      {
        "macaddress": "381F8D*"
      },
      {
        "macaddress": "508A06*"
      },
      {
        "macaddress": "68572D*"
      },
      {
        "macaddress": "708976*"
      },
      {
        "macaddress": "7CF666*"
      },
      {
        "macaddress": "84E342*"
      },
      {
        "macaddress": "D4A651*"
      },
      {
        "macaddress": "D81F12*"
      }
    ],
    "documentation": "https://github.com/azerty9971/xtend_tuya",
    "integration_type": "hub",
    "iot_class": "cloud_push",
    "issue_tracker": "https://github.com/azerty9971/xtend_tuya/issues",
    "loggers": [
      "tuya_iot",
      "tuya_sharing"
    ],
    "requirements": [
      "tuya-device-sharing-sdk==0.2.1",
      "tuya-iot-py-sdk==0.6.6"
    ],
    "version": "3.0.8",
    "is_built_in": false,
    "overwrites_built_in": false
  },
  "setup_times": {},
  "data": {
    "mqtt_connected": null,
    "disabled_by": null,
    "disabled_polling": false,
    "id": "bf2e905eea20e98690croh",
    "name": "Energy Recovery Ventilator ",
    "category": "xfj",
    "local_key": "BHZEHK<|3j{F+5l-",
    "product_id": "ics2husbxx7n6wz5",
    "product_name": "Energy Recovery Ventilator ",
    "online": true,
    "sub": false,
    "time_zone": "+01:00",
    "active_time": "2024-11-25T16:45:34+00:00",
    "create_time": "2024-11-25T16:45:34+00:00",
    "update_time": "2024-11-25T16:45:34+00:00",
    "function": {
      "switch": {
        "type": "Boolean",
        "value": {},
        "property_update": false,
        "accessMode": null,
        "dpId": 1
      },
      "child_lock": {
        "type": "Boolean",
        "value": {},
        "property_update": false,
        "accessMode": null,
        "dpId": 14
      }
    },
    "status_range": {
      "switch": {
        "type": "Boolean",
        "value": {},
        "property_update": false,
        "access_mode": null,
        "dpId": 1
      },
      "eco2": {
        "type": "Integer",
        "value": {
          "unit": "",
          "min": 0,
          "max": 8001,
          "scale": 0,
          "step": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 6
      },
      "humidity_indoor": {
        "type": "Integer",
        "value": {
          "unit": "%",
          "min": 0,
          "max": 101,
          "scale": 0,
          "step": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 8
      },
      "temp_indoor": {
        "type": "Integer",
        "value": {
          "unit": "\u2103",
          "min": -20,
          "max": 100,
          "scale": 0,
          "step": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 9
      },
      "filter_life": {
        "type": "Integer",
        "value": {
          "unit": "\u5929",
          "min": 0,
          "max": 200,
          "scale": 0,
          "step": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 11
      },
      "child_lock": {
        "type": "Boolean",
        "value": {},
        "property_update": false,
        "access_mode": null,
        "dpId": 14
      },
      "fault": {
        "type": "Raw",
        "value": {
          "label": [
            "e1"
          ],
          "type": "bitmap",
          "maxlen": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 18
      },
      "temp_outdoor": {
        "type": "Integer",
        "value": {
          "unit": "\u2103",
          "min": -20,
          "max": 100,
          "scale": 0,
          "step": 1
        },
        "property_update": false,
        "access_mode": null,
        "dpId": 22
      }
    },
    "status": {
      "switch": true,
      "eco2": 440,
      "humidity_indoor": 55,
      "temp_indoor": 17,
      "filter_life": 35,
      "child_lock": false,
      "fault": 0,
      "temp_outdoor": 9
    },
    "local_strategy": {
      "1": {
        "value_convert": "default",
        "status_code": "switch",
        "config_item": {
          "statusFormat": "{\"switch\": \"$\"}",
          "valueDesc": "{}",
          "valueType": "Boolean",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "6": {
        "value_convert": "default",
        "status_code": "eco2",
        "config_item": {
          "statusFormat": "{\"eco2\": \"$\"}",
          "valueDesc": "{\"unit\": \"\", \"min\": 0, \"max\": 8001, \"scale\": 0, \"step\": 1}",
          "valueType": "Integer",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "8": {
        "value_convert": "default",
        "status_code": "humidity_indoor",
        "config_item": {
          "statusFormat": "{\"humidity_indoor\": \"$\"}",
          "valueDesc": "{\"unit\": \"%\", \"min\": 0, \"max\": 101, \"scale\": 0, \"step\": 1}",
          "valueType": "Integer",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "9": {
        "value_convert": "default",
        "status_code": "temp_indoor",
        "config_item": {
          "statusFormat": "{\"temp_indoor\": \"$\"}",
          "valueDesc": "{\"unit\": \"\\u2103\", \"min\": -20, \"max\": 100, \"scale\": 0, \"step\": 1}",
          "valueType": "Integer",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "11": {
        "value_convert": "default",
        "status_code": "filter_life",
        "config_item": {
          "statusFormat": "{\"filter_life\": \"$\"}",
          "valueDesc": "{\"unit\": \"\\u5929\", \"min\": 0, \"max\": 200, \"scale\": 0, \"step\": 1}",
          "valueType": "Integer",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "14": {
        "value_convert": "default",
        "status_code": "child_lock",
        "config_item": {
          "statusFormat": "{\"child_lock\": \"$\"}",
          "valueDesc": "{}",
          "valueType": "Boolean",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "18": {
        "value_convert": "default",
        "status_code": "fault",
        "config_item": {
          "statusFormat": "{\"fault\": \"$\"}",
          "valueDesc": "{\"label\": [\"e1\"], \"type\": \"bitmap\", \"maxlen\": 1}",
          "valueType": "Raw",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      },
      "22": {
        "value_convert": "default",
        "status_code": "temp_outdoor",
        "config_item": {
          "statusFormat": "{\"temp_outdoor\": \"$\"}",
          "valueDesc": "{\"unit\": \"\\u2103\", \"min\": -20, \"max\": 100, \"scale\": 0, \"step\": 1}",
          "valueType": "Integer",
          "enumMappingMap": {},
          "pid": "ics2husbxx7n6wz5"
        },
        "status_code_alias": [],
        "property_update": false,
        "use_open_api": false
      }
    },
    "home_assistant": {
      "name": "Energy Recovery Ventilator ",
      "name_by_user": null,
      "disabled": false,
      "disabled_by": null,
      "entities": [
        {
          "disabled": false,
          "disabled_by": null,
          "entity_category": null,
          "device_class": null,
          "original_device_class": null,
          "icon": null,
          "original_icon": null,
          "unit_of_measurement": "",
          "state": {
            "entity_id": "sensor.energy_recovery_ventilator_concentrazione_di_anidride_carbonica",
            "state": "440.0",
            "attributes": {
              "state_class": "measurement",
              "unit_of_measurement": "",
              "friendly_name": "Energy Recovery Ventilator  Concentrazione di anidride carbonica"
            },
            "last_changed": "2025-01-29T22:24:42.279029+00:00",
            "last_reported": "2025-01-29T22:24:53.779885+00:00",
            "last_updated": "2025-01-29T22:24:42.279029+00:00"
          }
        },
        {
          "disabled": false,
          "disabled_by": null,
          "entity_category": null,
          "device_class": null,
          "original_device_class": "temperature",
          "icon": null,
          "original_icon": null,
          "unit_of_measurement": "\u00b0C",
          "state": {
            "entity_id": "sensor.energy_recovery_ventilator_temperatura",
            "state": "17.0",
            "attributes": {
              "state_class": "measurement",
              "unit_of_measurement": "\u00b0C",
              "device_class": "temperature",
              "friendly_name": "Energy Recovery Ventilator  Temperatura"
            },
            "last_changed": "2025-01-29T22:24:27.962973+00:00",
            "last_reported": "2025-01-29T22:24:53.780119+00:00",
            "last_updated": "2025-01-29T22:24:27.962973+00:00"
          }
        },
        {
          "disabled": false,
          "disabled_by": null,
          "entity_category": null,
          "device_class": null,
          "original_device_class": "humidity",
          "icon": null,
          "original_icon": null,
          "unit_of_measurement": "%",
          "state": {
            "entity_id": "sensor.energy_recovery_ventilator_umidita",
            "state": "55.0",
            "attributes": {
              "state_class": "measurement",
              "unit_of_measurement": "%",
              "device_class": "humidity",
              "friendly_name": "Energy Recovery Ventilator  Umidit\u00e0"
            },
            "last_changed": "2025-01-29T22:24:27.966722+00:00",
            "last_reported": "2025-01-29T22:24:53.780297+00:00",
            "last_updated": "2025-01-29T22:24:27.966722+00:00"
          }
        },
        {
          "disabled": false,
          "disabled_by": null,
          "entity_category": "config",
          "device_class": null,
          "original_device_class": null,
          "icon": null,
          "original_icon": null,
          "unit_of_measurement": null,
          "state": {
            "entity_id": "switch.energy_recovery_ventilator_interruttore",
            "state": "on",
            "attributes": {
              "friendly_name": "Energy Recovery Ventilator  Interruttore"
            },
            "last_changed": "2025-01-29T22:24:27.995419+00:00",
            "last_reported": "2025-01-29T22:24:53.780426+00:00",
            "last_updated": "2025-01-29T22:24:27.995419+00:00"
          }
        }
      ]
    },
    "set_up": true,
    "support_local": true,
    "data_model": {}
  }
}

for anyone else looking for this, i have a apollo ERV (Holtop Rebrand AKA Pioneer ecoasis 150). I was able to get it working fully with modes and fan speed using local tuya with the following template. I did not configure pm2.5 as my version doesnt appear to have the sensor as it always reports 0 but its easy to add it to the template if you need it.

- switch:
    friendly_name: 'Apollo ERV '
    entity_category: None
    restore_on_reconnect: false
    is_passive_entity: false
    device_class: switch
    id: '1'
    platform: switch
    icon: ''
- sensor:
    friendly_name: co2
    entity_category: diagnostic
    unit_of_measurement: ppm
    device_class: carbon_dioxide
    state_class: measurement
    id: '6'
    platform: sensor
    icon: ''
- sensor:
    friendly_name: Humidity
    entity_category: diagnostic
    unit_of_measurement: '%'
    device_class: humidity
    state_class: measurement
    id: '8'
    platform: sensor
    icon: ''
- select:
    friendly_name: Fan Speed
    entity_category: config
    select_options:
      '0': Manual
      '1': Auto
      '2': Sleep
      '3': Pure Low
      '4': Pure Medium
      '5': Pure High
    restore_on_reconnect: true
    is_passive_entity: false
    id: '2'
    platform: select
    icon: ''
- sensor:
    friendly_name: Indoor Temperature
    entity_category: diagnostic
    unit_of_measurement: Fahrenheit
    device_class: temperature
    state_class: measurement
    id: '9'
    platform: sensor
    icon: ''
- sensor:
    friendly_name: Filter days left before cleaning
    entity_category: diagnostic
    unit_of_measurement: Days
    state_class: measurement
    id: '11'
    platform: sensor
    icon: ''
- sensor:
    friendly_name: Filter status
    entity_category: diagnostic
    state_class: measurement
    id: '105'
    platform: sensor
    icon: ''
- button:
    friendly_name: Filter Reset
    entity_category: config
    id: '108'
    platform: button
    icon: ''
- sensor:
    friendly_name: Fault Code
    entity_category: diagnostic
    id: '18'
    platform: sensor
    icon: ''
- number:
    id: '103'
    friendly_name: Intake Fan Speed
    entity_category: config
    min_value: 0.0
    max_value: 8.0
    step_size: 1.0
    restore_on_reconnect: true
    is_passive_entity: false
    device_class: speed
    platform: number
- number:
    id: '104'
    friendly_name: Exhaust Fan Speed
    entity_category: config
    min_value: 0.0
    max_value: 8.0
    step_size: 1.0
    restore_on_reconnect: true
    is_passive_entity: false
    dps_default_value: '3'
    device_class: speed
    platform: number

How did you get your ERV into Home Assistant? I have the Pioneer Airlink app, which has a user code and when I scan it with the home assistant area to scan, it says to use the correct app.

What app did you use to get your Pioneer ERV into and scan the Home Assistant QR code?

Thanks for the help

Check my esphome config for holtop erv.