Zigbee2MQTT and TRV602z

I haven't used Zigbee before but I need local control of TRV's - my broadband has been poor over the last few months and relying on the Tuya cloud anymore isn't an option for me.

I've installed and run zigbee2mqtt on my RPI5. I use the same Pi for Node Red and MQTT broker, but I don't run Docker. I can run zigbee2mqtt manually by typing

sudo pnpm start 

from the /opt/zigbee2mqtt folder. However if I try running zigbee2mqtt as a service, I get the following response to

sudo systemctl status zigbee2mqtt.service

Ă— zigbee2mqtt.service - zigbee2mqtt
     Loaded: loaded (/etc/systemd/system/zigbee2mqtt.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2026-02-27 12:00:03 GMT; 34s ago
   Duration: 5ms
 Invocation: 9e7d3815f2fd4e6ebb7542f85c9c7825
    Process: 1055 ExecStart=/opt/zigbee2mqtt/pnpm start (code=exited, status=217/USER)
   Main PID: 1055 (code=exited, status=217/USER)

Feb 27 12:00:03 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Scheduled restart job, restart counter is at 5.
Feb 27 12:00:03 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Start request repeated too quickly.
Feb 27 12:00:03 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Failed with result 'exit-code'.
Feb 27 12:00:03 RPI5Node-Red systemd[1]: Failed to start zigbee2mqtt.service - zigbee2mqtt.

My zigbee2mqtt.service file looks like this

[Unit]
Description=zigbee2mqtt
After=network.target

[Service]
#ExecStart=/usr/bin/npm start
ExecStart=/opt/zigbee2mqtt/pnpm start
WorkingDirectory=/opt/zigbee2mqtt
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi

[Install]
WantedBy=multi-user.target

I'm also struggling to get my head around writing a device specific descriptor for the TRV602z.
I can see and control the TRV from the zigbee2mqtt dashboard and NR reports the TRV as connected and recognises its 'friendly name' but I can't get it to report status or issue any dp commands.
I've put a suggested file called trv602z.js in /opt/zigbee2mqtt (also in /data sub folder) which looks like this...

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const modernExtend = require('zigbee-herdsman-converters/lib/modernExtend');
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const e = exposes.presets;
const ea = exposes.access;

const fixValueConverter = {
    thermostatScheduleDayMultiDP_TRV601Z: {
        from: (v) => {
            const schedule = [];
            for (let in

and added

external_converters:
  - trv602z.js

at the end of configuration.yaml in the /opt/zigbee2mqtt/data folder, but this isn't doing anything.
Any help appreciated as always.

The trv602z.js file goes on beyond what is shown; I'm still working out how to include the whole file in my post....

Did you change that? Mine uses the first line that is commented out in yours.

Are you sure that is the right path to pnpm? Mine is in /usr/bin/pnpm.

Yes, I changed that. I just tried using the original line and the result is the same.

I saw a suggestion somewhere that it was user related hence the message I get when I run

sudo systemctl status zigbee2mqtt.service

(code=exited, status=217/USER)

Oh, I have just noticed that you are using sudo when you run it manually. You should not need to do that. You should be able to run it without sudo from the user that you used when you installed it. what does this command show?
ls -l /opt/zigbee2mqtt/data

Also
ls -l /opt/zigbee2mqtt

chrissaich@RPI5Node-Red:/opt/zigbee2mqtt/data $

ls -l /opt/zigbee2mqtt/data
total 40
-rw-r--r-- 1 root root  1491 Feb 25 12:10 configuration.example.yaml
-rw-r--r-- 1 root root   658 Feb 26 13:11 configuration.yaml
-rw-r--r-- 1 root root  1491 Feb 25 13:56 configuration.yml
-rw-r--r-- 1 root root   702 Feb 27 13:52 coordinator_backup.json
-rw-r--r-- 1 root root  3127 Feb 27 13:52 database.db
drwxr-xr-x 8 root root  4096 Feb 27 13:50 log
-rw-r--r-- 1 root root  1500 Feb 27 13:52 state.json
-rw-r--r-- 1 root root 10696 Feb 26 13:06 trv602z.js

and chrissaich@RPI5Node-Red:/opt/zigbee2mqtt/data $

ls -l /opt/zigbee2mqtt
total 636
-rw-r--r-- 1 root root  10847 Feb 25 12:10 AGENTS.md
-rw-r--r-- 1 root root   3448 Feb 25 12:10 biome.json
-rw-r--r-- 1 root root 304994 Feb 25 12:10 CHANGELOG.md
-rwxr-xr-x 1 root root    175 Feb 25 12:10 cli.js
-rw-r--r-- 1 root root   5447 Feb 25 12:10 CODE_OF_CONDUCT.md
-rw-r--r-- 1 root root   1350 Feb 25 12:10 CONTRIBUTING.md
drwxr-xr-x 3 root root   4096 Feb 27 13:52 data
drwxr-xr-x 6 root root   4096 Feb 25 12:17 dist
drwxr-xr-x 2 root root   4096 Feb 25 12:10 docker
drwxr-xr-x 2 root root   4096 Feb 25 12:10 images
-rw-r--r-- 1 root root   5248 Feb 25 12:10 index.js
drwxr-xr-x 6 root root   4096 Feb 25 12:10 lib
-rw-r--r-- 1 root root  35141 Feb 25 12:10 LICENSE
drwxr-xr-x 7 root root   4096 Feb 25 12:16 node_modules
-rw-r--r-- 1 root root   3304 Feb 25 12:10 package.json
-rw-r--r-- 1 root root  86164 Feb 25 12:10 pnpm-lock.yaml
-rw-r--r-- 1 root root   7291 Feb 25 12:10 README.md
-rw-r--r-- 1 root root    375 Feb 25 12:10 release-please-config.json
drwxr-xr-x 2 root root   4096 Feb 26 13:43 scripts
drwxr-xr-x 5 root root   4096 Feb 25 12:10 test
-rw-r--r-- 1 root root  10696 Feb 26 13:41 trv602z.js
-rw-r--r-- 1 root root    623 Feb 25 12:10 tsconfig.json
-rw-r--r-- 1 root root 101712 Feb 25 12:17 tsconfig.tsbuildinfo
-rw-r--r-- 1 root root    100 Feb 25 12:10 tsconfig.types.json
-rwxr-xr-x 1 root root   1626 Feb 25 12:10 update.sh

Which instructions did you follow to install it? They shouldn't be owned by root.

I used the instructions on zigbee2mqtt site for Linux

https://www.zigbee2mqtt.io/guide/installation/01_linux.html

I previously didn't have a user called 'pi'. I have just created one. The response from running
sudo systemctl status zigbee2mqtt has changed

Ă— zigbee2mqtt.service - zigbee2mqtt
     Loaded: loaded (/etc/systemd/system/zigbee2mqtt.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2026-02-27 15:10:23 GMT; 41s ago
   Duration: 499ms
 Invocation: b739fe601f9c4dae84153ebf349b0bd0
    Process: 1269 ExecStart=/usr/bin/pnpm start (code=exited, status=1/FAILURE)
   Main PID: 1269 (code=exited, status=1/FAILURE)

Feb 27 15:10:23 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Scheduled restart job, restart counter is at 5.
Feb 27 15:10:23 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Start request repeated too quickly.
Feb 27 15:10:23 RPI5Node-Red systemd[1]: zigbee2mqtt.service: Failed with result 'exit-code'.
Feb 27 15:10:23 RPI5Node-Red systemd[1]: Failed to start zigbee2mqtt.service - zigbee2mqtt.

Now Status 1/Failure,
Previously Status 217/USER

I assume you used sudo when running the install script, hence all the files are owned by root, which is why it is now failing. I wouldn't use a user called pi as that is the default on the pi and any hacker will try and attack that user. If you want to run it as your normal user, which appears to be chrissaich, then change all the files to be owned by yourself

cd /opt
sudo chown -R chrissaich:chrissaich zigbee2mqtt

then change the user in the service file to match that, and don't use sudo when starting it manually (or it will set any files that it writes back to root again).

Yep, that's worked. Thanks very much!

How are you on descriptor files for non-standard zigbee devices? (That was the second part of my original question :wink:

According to this it should be recognised by the standard software, though it seems that there are some issues. It should basically function, I think, without the workaround there (is that the one you have tried to use), so I would first get it going without the workaround. It should report its status without the workaround. Have you tried using MQTTExplorer, which is invaluable in such cases, as it will easily show you exactly what it is publishing.

Definitely getting closer.

I seem to be making progress by using the MQTT In and Out nodes. Whenever a change occurs on the TRV, an MQTT In node reports back and I can read;

zigbee2mqtt/TRV1 : msg.payload : Object
object
battery: 47
boost_timeset_countdown: 0
child_lock: "UNLOCK"
comfort_temperature: 19
current_heating_setpoint: 19.5
display_brightness: "high"
eco_temperature: 15
frost_protection: "ON"
holiday_temperature: 5
hysteresis: "comfort"
linkquality: 97
local_temperature: 18.2
local_temperature_calibration: 0
max_temperature: 30
min_temperature: 5
motor_thrust: "middle"
position: 61
preset: "auto"
running_mode: "auto"
running_state: "heat"
schedule_friday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_monday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_saturday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_sunday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_thursday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_tuesday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
schedule_wednesday: "06:00/19.0 08:00/15.0 12:00/19.0 14:00/15.0 18:00/19.0 22:00/15.0"
screen_orientation: "down"
system_mode: "auto"
window: "CLOSE"
window_detection: "OFF"

I need to read 'battery', 'position' and 'local_temperature'.
I want to set 'current_heating_setpoint'

I don't know how to extract the input values or how to set 'current_heating_setpoint'

That looks like a JavaScript object, so using a function node in Node-RED you could extract, for example the value of battery, with this...

let battery = msg.payload.battery;
msg.battery = battery;   // optional: store it elsewhere in msg
return msg;

You could do the same for 'position' and 'local_temperature'.

Or you can do the same using a Change node. Have a look at the node red docs 'working with messages'

Checkout Tuya TRV601 control via MQTT | Zigbee2MQTT

current_heating_setpoint: Temperature setpoint. To control publish a message to topic
zigbee2mqtt/FRIENDLY_NAME/set with payload {"current_heating_setpoint": VALUE}
where VALUE is the °C between 5 and 35. Reading (/get) this attribute is not possible

Also try subscribing to zigbee2mqtt/TRV1/# to see if there is a regular state update posted.

Note; You cannot 'get' or 'set' either battery or local_temperature they can only be read when a TRV update occurs. (as shown in your Definitely getting closer post and dynamicdave's example)

current_heating_setpoint can only be 'set', again it can only be read when the TRV updates

with an inject node

msg.topic = 'zigbee2mqtt/TRV1/set'
msg.payload = {"current_heating_setpoint": 20}

Thanks for all the help, guys, I'm up and running! :+1:

let battery = msg.payload.battery;
msg.payload = battery;   // optional: store it elsewhere in msg
return msg;
1 Like