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) | works | Pure local with cloud fallback for one rotating DP | |
| DBS2300 (older) | 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)
- Factory reset the unit
- Pair to the Tuya Smart app (not Dabbsson app)
- Create a Tuya IoT Platform project, link your Tuya Smart account
- Use
tinytuya wizardto extract device ID + local key - For DBS2000L: communicate via local LAN at protocol v3.5
- 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/propertiesendpoint 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.comover 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_errorcode1changes — they cause charge cycles to start/stop, which then triggers the bit changes. bms_errorcode1transitions reliably leadcharge_state(DP 101) by ~5 seconds. The status register reports the BMS's intent;charge_statereports 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.