BWT - Best Water Tech­nology - Support

Yes I am in UK on 2.0307 - 157 and can’t find a way to enable local API

So how come I have 2.200148 and no one else. On my version I can query only 2 endpoints: /silk/status and /silk/registers. The later is the one that provides the data but it comes as JSON array of the values. So I need to reverse engineer what is what. Some items are easy to guess but others - no way.

Can you try /silk/status and /silk/registers. Both on port 80

1 Like

I have emailed BWT support and the technical team said that the FW that’s on the UK devices does not provide local API.
I’m looking for a new water softener, but no local API means no purchase. :person_shrugging:

Brilliant- yes those endpoints work. How did you come across them?

My /registers is

{"params":[0,-1,18,46,285,29,16,2,0,9,8,1,8,4,297,150,0,50,2140,8,40,0,100,521,11,20,50,1,1,0,160,160,20,0,680,1,-1,-1,4,1,1,15,116,-1,-1,-1,-1,0]}

Array /silk/registers indexes that I’ve managed to work out:

2 = Current time - Hour in UTC
3 = Current time - Minute in UTC
7 = Recharge time - Hour (?)
8 = Recharge time - Minute (?)

19 = Number of recharges Matches cloud API value: "TotalRecharges": 9

14 = Water use - Litres - per day avg

15 = Water use? Mine says 159, which matches a value from cloud API that is "TotalWaterServed": 15900.0

16 = Current flow rate (Litres per sec)

42 = Water use - Litres - resets to zero overnight at 2am (recharge time?)

23 = Current Capacity litres - mine goes to 2630 after recharge (Perla Silk M), could work out % from this - assume this changes per model.

34 = coverage remaining (warranty? In days) Matches cloud API value: "DaysServiceExpire": 678

17 = Days in service (53 in my case) Matches cloud API value: "TotalDaysInService": 53

18 = some high value in days (2137 in my case), counting down each night (note: [17] + [18] = 2190 = number of days in 6 years - what’s special about 6 years on these devices?)

23 = high value that varies - potential input hardness? I’m in area of hard water I think ~ roughly 280ish on mg/L - very rough and this is hovering around 2150-2300.

30 = fixed value of 160 - salt max capacity - This is in kg * 10 from the spec sheet. Mine is M and rated at 16kg of salt capacity. Matches cloud API value: "MaxSaltCapacity": 16.0

31 = some value that only gets updated after recharge - was at 160 when at 100% salt, now 141 after recharge. Matches cloud API value: "CurrentSaltLevelKg": 14.1 - App is showing 88% - 141/160 is 88% so [30] is max and [31] is salt remaining so [31] divided by [30] is salt % remaining

I went to /web/ and then noticed that this page is pulling data from those 2 endpoints.

1 Like

Easy now you mention it - @otoo do you have any other indexes that you’ve worked out?

I will build a new (or add to the existing) BWT integration to support us on the newer firmwares.

1 Like

Interesting. Is there any content on /web that indicates what the values are?
If not, more datapoints are needed to reverse engineer the api. Would be helpful if more people post their responses.

Probably the (in/out) hardness is in there, also probably errors encoded as numbers.
For the water use you need to figure out whether they relate to blended water or fully desalinated water - there was quite a bit of confusion when developing the original integration due to a mix of them in the api response.

Happy to assist or add the api to the integration once we figured out the details.

There is also a debug log view but I think this is just the ESP32 retrieving the underlying values from the hardware sensors etc

/silk/getdebug

[
  "165651\t181008\t142460\t80320\tPolling register set 24, valid: 1, s: 0, e:0 ",
  "165651\t181008\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R1808",
  "165651\t181008\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr24,Count8 ",
  "165651\t181008\t142460\t80320\t000B ",
  "165651\t181008\t142460\t80320\t0014 ",
  "165651\t179296\t142460\t80320\t0032 ",
  "165651\t181008\t142460\t80320\t0001 ",
  "165651\t181008\t142460\t80320\t0001 ",
  "165651\t181008\t142460\t80320\t0000 ",
  "165651\t181008\t142460\t80320\t00A0 ",
  "165651\t181008\t142460\t80320\t008D ",
  "165651\t181008\t142460\t80320\t\r\n",
  "165653\t176872\t142460\t80320\tHandling web request: status",
  "165653\t172080\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
  "165653\t169720\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
  "165655\t167632\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
  "165655\t172076\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
  "165655\t173940\t142460\t80320\tHandling web request: status",
  "165656\t181004\t142460\t80320\t[SoftenerComms.cpp:119] ProcessSoftenerComms(): Polling register set 32",
  "165656\t181004\t142460\t80320\tPolling register set 32, valid: 1, s: 0, e:0 ",
  "165656\t181004\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R2008",
  "165656\t181004\t142460\t80320\tBOASTW3 is talking: 0014000002a600010001000000a0008d",
  "165656\t181004\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr32,Count8 ",
  "165656\t181004\t142460\t80320\t0014 ",
  "165656\t181004\t142460\t80320\t0000 ",
  "165656\t181004\t142460\t80320\t02A6 ",
  "165656\t181004\t142460\t80320\t0001 ",
  "165656\t181004\t142460\t80320\tFFFF ",
  "165656\t181004\t142460\t80320\tFFFF ",
  "165656\t181004\t142460\t80320\t0006 ",
  "165656\t181004\t142460\t80320\t0001 ",
  "165656\t181004\t142460\t80320\t\r\n",
  "165659\t169620\t142460\t80320\tHandling web request: status",
  "165659\t168836\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
  "165659\t169708\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
  "165661\t181008\t142460\t80320\t[SoftenerComms.cpp:119] ProcessSoftenerComms(): Polling register set 40",
  "165661\t181008\t142460\t80320\t[SoftenerComms.cpp:309] PollRegisters(): Sent command=$R2808",
  "165661\t177200\t142460\t80320\tHandling web request: status",
  "165661\t172800\t142460\t80320\t[SoftenerComms.cpp:288] FormatRegisterString(): FormatRegisterString: {\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-",
  "165661\t172112\t142460\t80320\t{\"params\":[0,-1,16,35,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2279,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,351,-1,-1,-1,-1,0]}",
  "165661\t181008\t142460\t80320\t[SoftenerComms.cpp:710] ParseRegValsFromHost(): ParseRegValsFromHost\r\n Addr40,Count8 ",
  "165661\t181008\t142460\t80320\t0001 ",
  "165661\t181008\t142460\t80320\t000F ",
  "165661\t181008\t142460\t80320\t015F ",
  "165661\t181008\t142460\t80320\tFFFF ",
  "165661\t181008\t142460\t80320\tFFFF ",
  "165661\t181008\t142460\t80320\tFFFF ",
  "165661\t181008\t142460\t80320\tFFFF ",
  "165661\t181008\t142460\t80320\t0000 ",
  "165661\t181008\t142460\t80320\t\r\n"
]

Nice one

I’ll continue to update the above post on indexes: I’ve worked out water use and salt % now so they’re the main ones I’d guess

Can you tell me what the native unit of measurement the BWT units tend to use for hardness? Is it dH or ppm or mg/L?

The Perla One and Duplex use dH everywhere. Edit: I checked and they actually publish 4 values: ppm caco3, dh, fH, mmol/l. I do calculations with dh, but guess it work with the others if they are different by just a factor.

They also have two different service counters, one performed by the customer twice per year, and the technician one every second year I think. But maybe the intervals are different, could be 6 years for yours.

BTW is there any authentication for these routes?

And have you figured out whether the water consumption is blended water (actually used liters in household) or fully 0dH water, like on the other Perla apis?

Totally unauthenticated.

The water consumption is just what goes through the softener unit - I haven’t configured any blending so my usage is pure soft - when I get a chance I’ll add some blend and see what the numbers diff by.

Here’s what I’m using currently (I can PR this to your repo when I’ve chance - would be good to build up a database of versions that support this endpoint!):

# BWT water softener
- scan_interval: 30
  resource: http://192.168.7.51/silk/registers 
  # {"params":[0,-1,18,23,285,29,16,2,0,9,8,1,8,4,296,158,0,52,2138,9,40,0,100,2275,11,20,50,1,1,0,160,141,20,0,678,1,-1,-1,6,1,1,15,355,-1,-1,-1,-1,0]}
  sensor:
    # Water usage sensors
    - name: "BWT Water Softener - Daily Average Water Use"
      value_template: "{{ value_json.params[14] }}"
      unit_of_measurement: "L"
      device_class: water
      icon: "mdi:water"
    - name: "BWT Water Softener - Total Water Served"
      value_template: "{{ (value_json.params[15] * 100) | round(0) }}"
      unit_of_measurement: "L"
      device_class: water
      state_class: total_increasing
      icon: "mdi:water"
    - name: "BWT Water Softener - Today Water Use"
      value_template: "{{ value_json.params[42] }}"
      unit_of_measurement: "L"
      device_class: water
      icon: "mdi:water"
    
    # Service and maintenance sensors
    - name: "BWT Water Softener - Days in Service"
      value_template: "{{ value_json.params[17] }}"
      unit_of_measurement: "d"
      device_class: duration
      state_class: total_increasing
      icon: "mdi:calendar-clock"
    - name: "BWT Water Softener - Total Recharges"
      value_template: "{{ value_json.params[19] }}"
      unit_of_measurement: "recharges"
      state_class: total_increasing
      icon: "mdi:counter"
    - name: "BWT Water Softener - Warranty Days Remaining"
      value_template: "{{ value_json.params[34] }}"
      unit_of_measurement: "d"
      device_class: duration
      state_class: measurement
      icon: "mdi:shield-clock"
    
    # Capacity and hardness sensors
    - name: "BWT Water Softener - Current Capacity"
      value_template: "{{ value_json.params[23] }}"
      unit_of_measurement: "L"
      device_class: water
      icon: "mdi:water-percent"
    - name: "BWT Water Softener - Current Capacity Percentage"
      value_template: "{{ ((value_json.params[23] / 2630) * 100) | round(0) }}"
      unit_of_measurement: "%"
      state_class: measurement
      icon: "mdi:water-percent"
    
    # Salt level sensors
    - name: "BWT Water Softener - Max Salt Capacity"
      value_template: "{{ (value_json.params[30] / 10) | round(1) }}"
      unit_of_measurement: "kg"
      device_class: weight
      icon: "mdi:shaker"
    - name: "BWT Water Softener - Current Salt Level"
      value_template: "{{ (value_json.params[31] / 10) | round(1) }}"
      unit_of_measurement: "kg"
      device_class: weight
      state_class: measurement
      icon: "mdi:shaker"
    - name: "BWT Water Softener - Salt Level Percentage"
      value_template: "{{ ((value_json.params[31] / value_json.params[30]) * 100) | round(0) }}"
      unit_of_measurement: "%"
      state_class: measurement
      icon: "mdi:percent"

These are some screenshots I managed to get from BWT UK customer support.

These are all my findings so far.

Index Value Meaning Notes / Confirmation
0 0 Salt Type Likely: 0 = Tablet salt. Could be enum.
1 -1 Unknown / Reserved Possibly unused or uninitialized.
2 23 Current Hour (UTC) :white_check_mark: Confirmed: Matches your local time (BST = UTC+1).
3 40 Current Minute (UTC) :white_check_mark: Confirmed.
4 300 Water Hardness (ppm) :white_check_mark: Matches cloud data (“Hardness”: 300).
5 30 Unknown – Possibly firmware or config ID Needs further confirmation.
6 17 Unknown – Possibly status flags May be a mode bitfield.
7 2 Recharge Time - Hour (UTC) :white_check_mark: Confirmed: You set this manually to 02:00.
8 0 Recharge Time - Minute (UTC) :white_check_mark: Confirmed: Matches recharge setting.
9 9 Unknown Might relate to recharge interval or other config.
10 8 Base Model Number :white_check_mark: Matches logs and cloud telemetry.
11 1 Duplex Setting 1 = Duplex mode enabled? Confirmed in message logs.
12 8 Unknown Possibly same as model ID. Needs clarification.
13 4 Turbine Pulses per Liter :white_check_mark: Confirmed. Used in flow metering.
14 109 Avg. Water Served per Day (litres) :white_check_mark: Confirmed. Matches cloud data.
15 426 Total Water Served (litres) :white_check_mark: Confirms summary message (totalServWater)
16 0 Failed Transfer Attempts :white_check_mark: Confirmed from cloud data.
17 387 Days in Service :white_check_mark: Matches daysInServ in cloud data.
18 1803 Warranty Days Remaining :white_check_mark: Matches cloud info.
19 22 Total Number of Recharges :white_check_mark: Confirmed.
20 40 Possibly recharge delay duration Might also relate to service schedule.
21 0 Unknown Could be error state, flag, or counter.
22 100 Salt Level (%) :white_check_mark: Confirmed.
23 1204 Remaining Capacity (litres) :white_check_mark: Matches remCap from cloud messages.
24 11 Possibly Flow Rate or usage counter Matches historical patterns.
25 20 Dwell Duration (minutes) :white_check_mark: Seen in debug logs.
26 50 Brine Duration (minutes) :white_check_mark: Confirmed.
27 1 Allow Changing Salt Type Flag :white_check_mark: Confirmed (1 = allowed).
28 1 Allow Changing Regen Time Flag :white_check_mark: Confirmed.
29 0 Unknown Possibly related to regeneration or schedule.
30 160 Possibly Max Capacity or threshold Needs monitoring.
31 100 Possibly Current Capacity % Matches salt % at times. Needs clarification.
32 20 Possibly minimum brine amount Needs checking.
33 0 Possibly Days Since Last Service 0 = could mean just serviced or reset.
34 343 Days Until Next Service :white_check_mark: Confirmed (daysServiceExp).
35–38 varies Unknown Could be error codes, counters, EEPROM diagnostics
39 1 Possibly telemetry / cloud sync flag Consistently 1 – transmission enabled?
40 1 Possibly telemetry / sync status Similar to above.
41 1 Possibly ‘Registered’ flag Consistent with /status.registered=true
42 15 Unknown Could be flow metric or counter.
43 57 Daily Water Usage (litres) :white_check_mark: Confirmed: WaterConsumption=57 from 1002 msg.
44–47 -1 Reserved / Unused Always -1. Uninitialized or inactive registers.
48 0 Unknown Possibly system status or reserved.

Nice one

[22] is not salt level for me - it is always 100 but the app shows 88% salt. This is worked out via [31]/[30] (remaining salt / max salt capacity)

[16] is current flow rate in L/s - not failed transfer attempts - I tested this morning empirically