Dabbsson DBS2000L / DBS2300 — full Tuya-based local + cloud integration findings

Dabbsson DBS2000L and DBS2300 — full local + cloud control findings (Tuya-based)

This is a writeup of what I've learned while building a local dashboard for two Dabbsson power stations (DBS2000L and DBS2300). Both are Tuya-based devices under the hood, but they behave very differently when you try to control them outside the Dabbsson app. Hopefully saves someone else a few hours.

TL;DR

Unit Local API Cloud API Recommended path
DBS2000L (newer) :white_check_mark: v3.5 protocol, full read+write works Pure local with cloud fallback for one rotating DP
DBS2300 (older) :cross_mark: rejects auth on every version works Cloud-only via Tuya's v2.0 properties endpoint

The big "aha" for me: Tuya's standard v1.0 getfunctions() API lies about which DPs are writable on the DBS2300. It reports only beep as writable. But using the v2.0 shadow/properties/issue endpoint, you can actually control AC output, USB, DC, charge target, LED, etc. — all the things the Tuya app uses.

Setup (high level)

  1. Factory reset the unit
  2. Pair to the Tuya Smart app (not Dabbsson app)
  3. Create a Tuya IoT Platform project, link your Tuya Smart account
  4. Use tinytuya wizard to extract device ID + local key
  5. For DBS2000L: communicate via local LAN at protocol v3.5
  6. For DBS2300: communicate via Tuya cloud REST API

Reverting to Dabbsson app requires another factory reset and re-pair.

DBS2000L (local control)

  • Local protocol: v3.5
  • Direct LAN, no internet needed (mostly — see DP 156 caveat below)
  • All read/write operations work

Readable DPs

DP Code Notes
1 battery_percentage always present
10 temp_current °C
25 beep bool
102 led_status enum: off, dim, bright, sos
109 ac_switch (AC output state) bool
111 dc12v_output_state bool — note: physically this controls BOTH DC and USB on the 2000L
112 usb_output_state PHANTOM DP — always reports a value but doesn't actually control anything on the 2000L. The 2000L only has one DC+USB combined hardware switch.
120 device_standby_time_set enum
121 lcd_off_time_set enum
122 ac_standby_time_set
123 custom_charge_power int (W) — settable
124 fast_or_slow_charge bool
127 device_mode enum
130 pd_verinfo firmware version string (PD subsystem)
132 bms_verinfo firmware version string (BMS)
133 inv_verinfo firmware version string (inverter)
145 output_vol_level 110 / 120
156 marge_value_dpid see DP 156 caveat
158 sale_log multi-line string with all power flow data — see DP 158 format
159 energy_in always 0 in my testing (firmware doesn't populate)
160 energy_out always 0 in my testing

DP 158 format (the only place real-time power data lives on the DBS2000L)

AC输入{V,A,W,Hz,enabled}
AC输出{V,A,W,parallel}
PV{V1,V2,A,W}        ← two voltage readings (PV1 and PV2)
INV电池端{V,A,W,setting}

Power flow you can derive from this:

  • AC input charging
  • AC output (the actual outlet draw — but NOT DC out, which the 2000L doesn't track separately)
  • Solar input
  • Battery (compute true W as |V × A|; the W field in this section is unreliable / often duplicates PV W)

DP 156 caveat — rotating buffer

DP 156 (marge_value_dpid) is a base64-encoded binary blob containing a rotating subset of other DPs. Format:

repeating: 1 byte DP id + 4 byte int32 big-endian

Different reads contain different DPs. The full set includes:

  • Per-cell battery voltages (DPs 77–~92, value in mV, LiFePO4 range 2500-3700)
  • DP 2 (remain_time in minutes — yes, the 2000L only exposes remain time inside this rotating buffer!)
  • Several other readings

Critical: the local protocol doesn't include DP 156 in routine status deltas — only the first full snapshot, and rarely after that. To get cell data and remain_time reliably, you need to either:

  • Poll the cloud /v2.0/cloud/thing/{id}/shadow/properties endpoint periodically just for this DP, or
  • Cache aggressively (10+ min TTL) and accept gaps

I went with the second approach + cloud fallback when local lacks the DP. Cells should be merged in cache (not replaced) so they accumulate to a full 16-cell readout over time.

What's NOT exposed on the DBS2000L

  • DC output wattage (DP 110 exists but is always 0)
  • USB per-port wattage
  • Energy in/out totals (DPs 159, 160 exist but always 0)
  • Detailed power breakdown per outlet

DBS2300 (cloud-only)

The DBS2300 actively rejects all Tuya local protocol versions (v3.1 through v3.5). Confirmed both reads and writes fail with auth errors. All control must go via cloud.

Why the v1.0 API is misleading

Calling cloud.getfunctions(device_id) returns ONLY beep as writable. But all of these actually work via cloud:

DP Code Type
25 beep writable
102 led_status enum: off, dim, bright, sos — writable
109 ac_output_state bool — writable
111 dc12v_output_state bool — writable
112 usb_output_state bool — writable
123 custom_charge_power int W — writable
124 fast_or_slow_charge bool — writable
144 app_heat bool — writable (cold weather battery preheat)
145 output_vol_level 110/120 — writable

The trick — use v2.0 endpoints

For reads:

GET /v2.0/cloud/thing/{device_id}/shadow/properties

Returns ALL DPs the device knows about, not the filtered schema list.

For writes:

POST /v2.0/cloud/thing/{device_id}/shadow/properties/issue
body: {"properties": "{\"code_name\": value}"}

This bypasses the v1.0 schema check and just sends the DP. Works for any DP the device firmware actually accepts.

DBS2300 readable DPs (selection)

DP Code Notes
1 battery_percentage
2 remain_time minutes
10 temp_current
103 display_mppt_input_power solar W
104 dispaly_acpower_input AC charging W
108 dispaly_ac_outpower AC outlet draw W
110 display_12v_outpower DC out W — works on the 2300, unlike the 2000L
113-118 display_usba1/2/3_outpower, display_usbc1/2/3_outpower per-port USB W (rounds to nearest W, so loads under ~1W show as 0)
127 device_mode
134 fault_type
136 bms_errorcode
137 bms_errorcode1 misnamed — actually a charge-state bitmap. See section below.
130/132/133 firmware version strings
140 serial_number

DP 156 (marge_value_dpid) on the 2300 contains a different rotating subset than the 2000L (more live power data, less cell info) — cells didn't show up in any of my samples on this unit.

Useful side discoveries

  • Per-cell battery voltages (DBS2000L only): a healthy 16S LiFePO4 pack should show cells within ~10-20 mV of each other. Worth monitoring for drift.
  • TLS encrypted cloud traffic: can't sniff and parse the DBS2300's cloud communication without firmware extraction. The device talks to mq.tuyaus.com over MQTT/TLS.
  • Tuya's free tier IoT Core: enough headroom for a polling dashboard (60 polls/min ≈ 86k/day, well under the daily quota).
  • OTA updates: Tuya Smart's "update available" prompt for the DBS2300 looped forever for me — firmware versions never actually changed across multiple "successful" update cycles. Dabbsson's issue, nothing fixable from the integrator side.

Caveats

  • Pairing with Tuya Smart breaks compatibility with the Dabbsson app until you factory reset again.
  • Local key changes every time you re-pair, so save it after each pairing.
  • DP IDs and meanings can vary between firmware revisions — verify on your specific unit.
  • See the dedicated bms_errorcode1 section below — it's not an error code at all, despite the name.

bms_errorcode1 (DP 137) is a charge-state machine, not an error register

This was the biggest surprise. The DP is named bms_errorcode1 so any naive integration treats it as an error code and fires alerts whenever it's non-zero. In practice it's a status bitmap tracking the BMS's charge-cycle state machine. I logged 6+ hours of state transitions while scripted-cycling AC/USB outputs and the pattern was very consistent.

Decoded bit meanings:

Bit Hex Meaning
64 0x40 Standby / ready (set during idle steady state)
128 0x80 Actively charging (any source — AC or PV)
256 0x100 Transition complete / cycle settled

Low bits (1, 2, 4, 8, 16, 32) were never set in any sample during normal operation. Those are presumably where real faults would show up.

Typical charge cycle sequence:

320 (64+256)      ← idle, ready, settled (default steady state)
  ↓ charge begins
64 only           ← "preparing to charge" transition
  ↓ ~6-10s
128 only          ← actively charging (charge_state DP 101 also flips True ~5s later)
  ↓ charge ends
320 (64+256)      ← back to idle/settled

Observations:

  • AC/USB output toggle events don't directly drive bms_errorcode1 changes — they cause charge cycles to start/stop, which then triggers the bit changes.
  • bms_errorcode1 transitions reliably lead charge_state (DP 101) by ~5 seconds. The status register reports the BMS's intent; charge_state reports the result.
  • Brief excursions to value 0 happen during fast transitions (all status flags momentarily off). Harmless.

Practical recommendation for integrations:

  • Treat values 64, 128, 192, 256, 320, 384, 448 as benign status — these are combinations of bits 6, 7, 8 that represent normal operating states.
  • Only alert if a low bit (1-32) sets. That's where real faults like cell over-voltage, BMS communication errors, temperature trips would likely live.
  • Don't waste user time with notifications when the BMS just cycles through normal states.