Browse Source

Add Tongou TOSMR1-40 reclosing RCBO (40 A variant) (#5704)

Adds support for the 40 A variant of the Tongou TOSMR1 Wi-Fi reclosing
RCBO.

### Why a new config instead of a `products` entry on the existing one

`uv run duplicates` reports a **100% structural match** with
`tongou_towsmr1-20_rcbo_breaker`, so per AGENTS.md here is the
explanation of the
differences:

The two configs are identical in structure — same dps, same entities.
They differ only in:

1. **Product identity** — my device reports product id
`f1q6h4xhddjnopxu`
   (`category: dlq`, product name "Breaker"), not `hktsllupwmoii1it`.
2. **Overcurrent threshold range (dps 114)** — `max: 20` → `max: 40`.

Adding my product id to the existing config would have capped the
overcurrent threshold at
20 A on a 40 A breaker. In practice this is not cosmetic: with the 20 A
config the threshold
gets constrained and the breaker trips when the load exceeds 20 A. My
device is protecting an
EV charger circuit that draws up to 32 A.

Raising `max` to 40 on the shared config did not seem right either, as
it would allow a 40 A
overcurrent threshold to be set on 20 A hardware.

If you prefer a single config with the product id added and a wider
range, I'm happy to
rework it that way — see the note on the manufacturer's table below,
which suggests the
firmware may accept the same range on both models.

### Device details

- **Product ID**: `f1q6h4xhddjnopxu`
- **Category**: `dlq`
- **Manufacturer / model**: Tongou TOSMR1-40 (Wi-Fi variant)
- **Discovered via**: cloud assisted setup

### Threshold ranges verified

All six thresholds in the config were checked against the manufacturer's
manual

([Smart-Circuit-Breaker-TOSMR1-Series.pdf](https://elcb.net/wp-content/uploads/2024/04/Smart-Circuit-Breaker-TOSMR1-Series.pdf))
and against what the SmartLife app offers for this unit:

| Setting | dps | Existing 20 A config | Manual / app | Change |
|---|---|---|---|---|
| Overcurrent | 114 | 1–20 A | TOSMR1-40: 20–40 A · app offers 1–40 A |
**20 → 40** |
| Overvoltage | 115 | 245–295 V | 245–295 V (default 280) | none |
| Undervoltage | 116 | 145–220 V | 145–220 V (default 165) | none |
| Leakage | 117 | 30–100 mA | app offers 30–100 mA | none |
| Temperature | 118 | −25–100 °C | — | none |
| Overpower | 119 | 5–24995 W | — | none |

Note: the manual's table reads
`TOSMR1-40: 20 - 40A (default 40A)` and `TOSMR1-20: 1 - 40A (default
20A)`, which looks like
it may contain a typo. The SmartLife app on my TOSMR1-40 offers 1–40 A,
so I used `min: 1`
to stay consistent with the existing config.

### Cross-check against the cloud data model

The Tuya cloud only exposes 6 dps for this device
(1 `forward_energy_total`, 6 `phase_a`, 15 `leakage_current`, 16
`switch`,
109 `online_state`, 142 `relay_status`). All of them match what the
existing config expects,
which confirms the mapping. The protection threshold dps (114–119) are
not exposed by the
cloud at all — they are local-only, which is exactly why this
integration is valuable here.

### Checks run

- `uv run yamllint custom_components/tuya_local/devices` — clean
- `uv run pytest tests/test_device_config.py` — 32 passed
- `uv run duplicates …` — 100% match with the 20 A config, explained
above

---------

Co-authored-by: Miguel Fuentes <miguel.fuentes@sharksia.com>
Co-authored-by: Jason Rumney <make-all@users.noreply.github.com>
fuentesdelafuente 1 day ago
parent
commit
929c3d0e1c

+ 428 - 0
custom_components/tuya_local/devices/tongou_towsmr140rcbo_breaker.yaml

@@ -0,0 +1,428 @@
+name: Circuit breaker
+products:
+  - id: f1q6h4xhddjnopxu
+    manufacturer: Tongou
+    model: tongou 40A Wi-Fi Smart Reclosing RCBO
+    model_id: TOSMR1-40JWT
+entities:
+  - entity: sensor
+    class: energy
+    category: diagnostic
+    dps:
+      - id: 1
+        type: integer
+        name: sensor
+        unit: kWh
+        class: total_increasing
+        mapping:
+          - scale: 100
+  - entity: sensor
+    class: voltage
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        unit: V
+        class: measurement
+        force: true
+        optional: true
+        mask: "FFFF000000000000"
+        mapping:
+          - scale: 10
+  - entity: sensor
+    class: current
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        unit: A
+        class: measurement
+        force: true
+        optional: true
+        mask: "0000FFFFFF000000"
+        mapping:
+          - scale: 1000
+  - entity: sensor
+    class: power
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        unit: kW
+        class: measurement
+        force: true
+        optional: true
+        mask: "0000000000FFFFFF"
+        mapping:
+          - scale: 1000
+  - entity: sensor
+    name: Leakage
+    class: current
+    category: diagnostic
+    dps:
+      - id: 15
+        type: integer
+        name: sensor
+        unit: mA
+        class: measurement
+  - entity: switch
+    translation_key: breaker
+    dps:
+      - id: 16
+        type: boolean
+        name: switch
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Overvoltage
+    category: config
+    dps:
+      - id: 102
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Undervoltage
+    category: config
+    dps:
+      - id: 103
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Overcurrent
+    category: config
+    dps:
+      - id: 104
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Overpower
+    category: config
+    dps:
+      - id: 105
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Temperature
+    category: config
+    dps:
+      - id: 107
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: select
+    translation_key: breaker_trigger_action
+    translation_placeholders:
+      trigger: Leakage
+    category: config
+    dps:
+      - id: 108
+        type: string
+        name: option
+        mapping:
+          - dps_val: Closed
+            value: ignore
+          - dps_val: Alarm
+            value: alarm
+          - dps_val: Trip
+            value: trip_breaker
+  - entity: button
+    name: Refresh
+    icon: "mdi:sync"
+    category: config
+    dps:
+      - id: 109
+        type: string
+        optional: true
+        name: button
+        mapping:
+          - dps_val: online
+            value: true
+  - entity: binary_sensor
+    class: connectivity
+    category: diagnostic
+    deprecated: button_refresh  # 2026-06-29
+    dps:
+      - id: 109
+        type: string
+        name: sensor
+        force: true
+        optional: true
+        mapping:
+          - dps_val: online
+            value: true
+          - dps_val: offline
+            value: false
+  - entity: sensor
+    translation_key: status
+    class: enum
+    category: diagnostic
+    dps:
+      - id: 110
+        type: string
+        name: sensor
+        mapping:
+          - dps_val: Normal
+            value: normal
+          - dps_val: Over_Current_Trip
+            value: tripped_by_overcurrent
+          - dps_val: Over_Power_Trip
+            value: tripped_by_overpower
+          - dps_val: High_Temp_Trip
+            value: tripped_by_high_temperature
+          - dps_val: Over_Voltage_Trip
+            value: tripped_by_overvoltage
+          - dps_val: Undervoltage Trip
+            value: tripped_by_undervoltage
+          - dps_val: Over_Current_Alarm
+            value: overcurrent_alarm
+          - dps_val: Over_Power_Alarm
+            value: overpower_alarm
+          - dps_val: High_Temp_Alarm
+            value: high_temperature_alarm
+          - dps_val: Over_Voltage_Alarm
+            value: overvoltage_alarm
+          - dps_val: Under_Voltage_Alarm
+            value: undervoltage_alarm
+          - dps_val: Remote_ON
+            value: switched_on_remotely
+          - dps_val: Remote_OFF
+            value: switched_off_remotely
+          - dps_val: Manual_ON
+            value: switched_on_manually
+          - dps_val: Manual_OFF
+            value: switched_off_manually
+          - dps_val: Leakage_Trip
+            value: tripped_by_leakage
+          - dps_val: Leakage_Alarm
+            value: leakage_alarm
+          - dps_val: Restore_Default
+            value: resetting
+          - dps_val: Automatic_Closing
+            value: automatic_closing
+          - dps_val: Electricity_Shortage
+            value: prepaid_electricity_used
+          - dps_val: Timing_switch_ON
+            value: switched_on_by_timer
+          - dps_val: Timing_switch_OFF
+            value: switched_off_by_timer
+  - entity: switch
+    name: Overvoltage recloser
+    category: config
+    dps:
+      - id: 112
+        type: boolean
+        name: switch
+  - entity: switch
+    name: Undervoltage recloser
+    category: config
+    dps:
+      - id: 112
+        type: boolean
+        name: switch
+  - entity: button
+    translation_key: factory_reset
+    category: config
+    hidden: true
+    dps:
+      - id: 113
+        type: boolean
+        name: button
+        optional: true
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Overcurrent
+    class: current
+    category: config
+    dps:
+      - id: 114
+        type: integer
+        name: value
+        unit: A
+        range:
+          min: 1
+          max: 40
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Overvoltage
+    class: voltage
+    category: config
+    dps:
+      - id: 115
+        type: integer
+        name: value
+        unit: V
+        range:
+          min: 245
+          max: 295
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Undervoltage
+    class: voltage
+    category: config
+    dps:
+      - id: 116
+        type: integer
+        name: value
+        unit: V
+        range:
+          min: 145
+          max: 220
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Leakage
+    class: current
+    category: config
+    dps:
+      - id: 117
+        type: integer
+        name: value
+        unit: mA
+        range:
+          min: 30
+          max: 100
+        mapping:
+          - step: 5
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Temperature
+    class: temperature
+    category: config
+    dps:
+      - id: 118
+        type: integer
+        name: value
+        unit: C
+        range:
+          min: -250
+          max: 1000
+        mapping:
+          - scale: 10
+  - entity: number
+    translation_key: sensor_threshold
+    translation_placeholders:
+      sensor: Overpower
+    class: power
+    category: config
+    mode: slider
+    dps:
+      - id: 119
+        type: integer
+        name: value
+        precision: 0
+        mapping:
+          - step: 10
+        range:
+          min: 5
+          max: 24995
+        unit: W
+  - entity: sensor
+    translation_key: energy_consumed
+    class: energy
+    category: diagnostic
+    dps:
+      - id: 125
+        type: integer
+        name: sensor
+        unit: kWh
+        class: total_increasing
+        range:
+          min: -999999999
+          max: 999999999
+        mapping:
+          - scale: 100
+  - entity: sensor
+    class: temperature
+    category: diagnostic
+    dps:
+      - id: 131
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        mapping:
+          - scale: 10
+  - entity: select
+    translation_key: initial_state
+    category: config
+    dps:
+      - id: 142
+        type: string
+        name: option
+        mapping:
+          - dps_val: "off"
+            value: "off"
+          - dps_val: "on"
+            value: "on"
+          - dps_val: memory
+            value: memory
+  - entity: switch
+    name: Overcurrent recloser
+    category: config
+    dps:
+      - id: 143
+        type: boolean
+        name: switch
+  - entity: switch
+    name: Leakage recloser
+    category: config
+    dps:
+      - id: 144
+        type: boolean
+        name: switch
+  - entity: switch
+    name: Overpower recloser
+    category: config
+    dps:
+      - id: 145
+        type: boolean
+        name: switch